Skip to content
DA DataAcuity by The Geek Network

GeoGlobal — API Reference

Complete reference for every endpoint and MCP tool. Two surfaces, same backing data.

  • REST APIhttps://maps.dataacuity.co.za/api/v2/* (proxied through maps_api) — the recommended path for TagMe and Takemehome
  • MCP servergeo_mcp:8000 in-cluster, or http://197.97.200.106:5026/sse externally — the recommended path for B! / Butler and any AI agent
  • Direct Valhallavalhalla:8002 in-cluster — only call directly if you need raw access to features the REST proxy doesn't expose (e.g., isochrones, height service, optimized_route)

Auth: The REST API expects the same DataAcuity Keycloak bearer token your app already sends. The MCP server is unauthenticated inside the network and should not be exposed publicly without an auth layer.


Table of contents

  1. REST Endpoints
  2. MCP Tools
  3. Common types
  4. Errors
  5. Rate limits & caching

REST Endpoints

All REST endpoints return JSON. All accept and return UTF-8. Coordinates are decimal degrees (WGS-84). Distances are kilometres unless noted.

The /api/v2/* paths are NEW. They are not yet wired into maps_api — see GeoGlobal_Deployment.md for the proxy implementation plan. Until that ships, apps can call the MCP server directly over HTTP+SSE (see MCP Tools) or hit the Valhalla container at :5027 for routing.

GET /api/v2/geocode

Find places by exact name. Returns top matches ranked by population.

Query parameters

Name Type Default Notes
query string required Place name (case-insensitive, exact match)
limit int 5 Max results, 1–50
include_interesting bool false If true, each result includes a nested interesting_nearby list
within_km float 10.0 POI search radius when include_interesting=true
max_interesting int 5 Cap on POIs returned per place

Example

curl 'https://maps.dataacuity.co.za/api/v2/geocode?query=Cape%20Town&include_interesting=true&max_interesting=3'

Response (200)

[
  {
    "name": "Cape Town",
    "country": "ZA",
    "fclass": "P",
    "fcode": "PPLA",
    "population": 4772846,
    "lat": -33.92584,
    "lng": 18.42322,
    "interesting_nearby": [
      {
        "name": "Castle Military Museum",
        "description": "Museum at the Castle of Good Hope...",
        "category": "tourism",
        "subcategory": "museum",
        "source": "osm",
        "country": "ZA",
        "lat": -33.9258,
        "lng": 18.4275,
        "interest_score": 0.85,
        "tags": ["tourism", "museum"],
        "meters": 361
      }
    ]
  }
]

Empty-result behavior: returns []. Never null.

GET /api/v2/reverse-geocode

Find the nearest populated places to a coordinate.

Query parameters

Name Type Default Notes
lat float required Latitude (–90 to 90)
lng float required Longitude (–180 to 180)
limit int 5 Max results, 1–50
include_interesting bool false If true, each result includes interesting_nearby
within_km float 10.0 POI search radius when include_interesting=true
max_interesting int 5 Cap on POIs per place

Example

curl 'https://maps.dataacuity.co.za/api/v2/reverse-geocode?lat=-33.9249&lng=18.4241&limit=3'

Response (200) — places sorted by distance ascending, with a meters field:

[
  {
    "name": "Cape Town",
    "country": "ZA",
    "fclass": "P",
    "population": 4772846,
    "lat": -33.92584,
    "lng": 18.42322,
    "meters": 88
  },
  {
    "name": "Sea Point",
    "country": "ZA",
    "fclass": "P",
    "population": 9132,
    "lat": -33.91667,
    "lng": 18.38333,
    "meters": 3914
  }
]

GET /api/v2/search

Fuzzy place search using PostgreSQL trigram similarity. Use this for partial input, autocomplete, and misspellings.

Query parameters

Name Type Default Notes
query string required Partial or approximate name
limit int 10 Max results, 1–50
include_interesting bool false Same as geocode
within_km float 10.0
max_interesting int 5

Example

curl 'https://maps.dataacuity.co.za/api/v2/search?query=Johanesburg&limit=3'

Response (200) — note the sim similarity score (0–1, 1 = exact):

[
  {
    "name": "Johannesburg",
    "country": "ZA",
    "fclass": "P",
    "population": 9418183,
    "lat": -26.20227,
    "lng": 28.04363,
    "sim": 0.846
  }
]

Recommended: filter UI suggestions to sim >= 0.4 to avoid noise.

GET /api/v2/poi/nearby

Find interesting POIs near a coordinate. Use this when you already have a location and want to enrich it (without geocoding first).

Query parameters

Name Type Default Notes
lat float required
lng float required
within_km float 10.0 Search radius, max 100
limit int 10 1–50
theme string none Optional tag filter (e.g. history, nature, must-see, architecture, religion)

Example

curl 'https://maps.dataacuity.co.za/api/v2/poi/nearby?lat=-33.9249&lng=18.4241&within_km=5&theme=history&limit=5'

Response (200) — POIs ranked by interest_score DESC, distance ASC:

[
  { "name": "Old Pump House",   "category": "historic", "interest_score": 0.88, "meters": 6365, "lat": -33.97, "lng": 18.41, "tags": ["historic"] },
  { "name": "Woodhead Tunnel",  "category": "historic", "interest_score": 0.88, "meters": 7115, "lat": -33.99, "lng": 18.43, "tags": ["historic"] },
  { "name": "Castle Military Museum", "category": "tourism", "interest_score": 0.85, "meters": 361 }
]

POST /api/v2/route

Turn-by-turn directions between two coordinates. Africa only.

Request body

{
  "from_lat": -33.9249, "from_lng": 18.4241,
  "to_lat": -26.2041,  "to_lng": 28.0473,
  "mode": "auto"
}

mode is one of: auto (default, driving), bicycle, pedestrian, motorcycle, truck.

Example

curl -X POST 'https://maps.dataacuity.co.za/api/v2/route' \
  -H 'Content-Type: application/json' \
  -d '{"from_lat":-33.9249,"from_lng":18.4241,"to_lat":-26.2041,"to_lng":28.0473,"mode":"auto"}'

Response (200)

{
  "distance_km": 1399.32,
  "duration_min": 733.0,
  "mode": "auto",
  "maneuvers": [
    {
      "instruction": "Drive east on Adderley Street.",
      "distance_km": 0.144,
      "duration_min": 0.4,
      "street_names": ["Adderley Street"]
    },
    { "instruction": "Turn left onto Strand Street.", "distance_km": 1.8, "duration_min": 3.2, "street_names": ["Strand Street"] }
  ]
}

Outside-Africa behavior: returns 400 with body {"error_code":171,"error":"No suitable edges near location"}.

POST /api/v2/quest

The synthesis tool. Finds the most interesting POIs near a starting point and routes through them in optimal order. Returns an ordered visit list plus total distance/time.

Request body

{
  "lat": -33.9249,
  "lng": 18.4241,
  "theme": "history",
  "within_km": 20,
  "max_pois": 5,
  "mode": "auto"
}
Field Type Default Notes
lat / lng float required Quest start (also where the quest ends — it's a round-trip)
theme string none Optional tag filter
within_km float 20 POI search radius, max 100
max_pois int 5 1–10 recommended
mode string auto auto, bicycle, pedestrian

Example

curl -X POST 'https://maps.dataacuity.co.za/api/v2/quest' \
  -H 'Content-Type: application/json' \
  -d '{"lat":-33.9249,"lng":18.4241,"theme":"history","within_km":10,"max_pois":5,"mode":"auto"}'

Response (200)

{
  "total_distance_km": 24.6,
  "total_duration_min": 78.0,
  "ordered_pois": [
    { "name": "Castle Military Museum", "category": "tourism", "interest_score": 0.85, "lat": -33.9258, "lng": 18.4275, "meters_from_start": 361 },
    { "name": "South African Maritime Museum", "category": "tourism", "interest_score": 0.85 },
    { "name": "Old Pump House", "category": "historic", "interest_score": 0.88 }
  ],
  "leg_count": 6,
  "mode": "auto",
  "theme": "history"
}

If no POIs match within radius: returns {"error":"no POIs found nearby", ...} with HTTP 200.

If POIs were found but Valhalla routing failed (e.g. outside Africa): returns {"ordered_pois":[...], "routing_error":"...", "mode":"auto"} — caller still has the POI list.


MCP Tools

The MCP server (geo_mcp container) exposes nine MCP tools — for B!/Butler and any other MCP-aware client.

Transport: SSE at http://geo_mcp:8000/sse (inside cluster) or http://197.97.200.106:5026/sse (external). Server name: geo-global. Current version: v0.5 (2026-05-28 — adds interesting_nearby, isochrone, distance_matrix, snap_to_road).

Place lookup

Tool Args Returns
geocode query: str, limit=5, include_interesting=False, within_km=10.0, max_interesting=5 list[Place]
reverse_geocode lat: float, lng: float, limit=5, include_interesting=False, within_km=10.0, max_interesting=5 list[Place]
search_places query: str, limit=10, include_interesting=False, within_km=10.0, max_interesting=5 list[Place]

POI discovery

Tool Args Returns
interesting_nearby lat: float, lng: float, within_km=10.0, limit=10, theme: str \| None = None list[Poi]

theme examples: "history", "nature", "must-see", "architecture", "religion", "tourism", "museum", "viewpoint". Bounds: within_km clamped to [0.1, 100], limit clamped to [1, 50].

Africa routing & reachability

Tool Args Returns
route from_lat, from_lng, to_lat, to_lng, mode='auto' RouteResult
discover_quest lat, lng, theme=None, within_km=20.0, max_pois=5, mode='auto' QuestResult
isochrone lat, lng, contours_minutes: list[int]=[15], mode='auto', denoise=0.5, generalize_meters=100 IsochroneResult
distance_matrix sources: list[{lat,lng}], targets: list[{lat,lng}], mode='auto' MatrixResult
snap_to_road lat, lng, mode='auto', radius_meters=500 SnapResult

mode accepts: "auto" (driving, default), "bicycle", "pedestrian", "motorcycle", "truck". Some tools also accept "bus", "taxi", "motor_scooter" where Valhalla supports them.

Schemas match the REST shapes above. The MCP server file at /home/geektrading/geo-mcp/server.py on .106 is the source of truth.

isochrone example call

{
  "lat": -33.9249, "lng": 18.4241,
  "contours_minutes": [10, 20, 30],
  "mode": "auto"
}

Returns:

{
  "center":   {"lat": -33.9249, "lng": 18.4241},
  "mode":     "auto",
  "contours_minutes": [10, 20, 30],
  "polygons": [
    {"time_minutes": 10, "geometry": {"type": "Polygon", "coordinates": [[...]]}},
    {"time_minutes": 20, "geometry": {"type": "Polygon", "coordinates": [[...]]}},
    {"time_minutes": 30, "geometry": {"type": "Polygon", "coordinates": [[...]]}}
  ]
}

Polygons are sorted smallest-contour first so a renderer can draw outer-to-inner. Caps: each contour ≤ 180 min; polygons always returns GeoJSON Polygon or MultiPolygon.

distance_matrix example call

{
  "sources": [{"lat": -33.92, "lng": 18.42}, {"lat": -26.20, "lng": 28.04}],
  "targets": [{"lat": -29.86, "lng": 31.02}, {"lat": -25.74, "lng": 28.19}],
  "mode": "auto"
}

Returns:

{
  "mode": "auto",
  "sources_count": 2,
  "targets_count": 2,
  "matrix": [
    [{"distance_km": 1657.3, "duration_min": 1011.8}, {"distance_km": 1399.5, "duration_min": 854.0}],
    [{"distance_km":  571.7, "duration_min":  366.4}, {"distance_km":   62.4, "duration_min":   54.6}]
  ]
}

matrix[i][j] is sources[i] → targets[j]. null for either field means unreachable (e.g. across an ocean, outside Africa). Capped at 50 × 50 to protect the routing container.

snap_to_road example call

{ "lat": -33.92485, "lng": 18.42105, "mode": "auto" }

Returns:

{
  "input":            {"lat": -33.92485, "lng": 18.42105},
  "snapped":          true,
  "snapped_lat":      -33.92489,
  "snapped_lng":      18.42098,
  "distance_meters":  7,
  "road": {
    "name":             "Adderley Street",
    "alternate_names":  [],
    "road_class":       "primary",
    "speed_kph":        50,
    "way_id":           5874021,
    "use":              "road"
  }
}

If no road within radius_meters (default 500, max 5000): {"snapped": false, "reason": "no routable edge within radius"}.

MCP client registration example (for B! / Butler):

// in your MCP client config
{
  "mcpServers": {
    "geo-global": {
      "url": "http://geo_mcp:8000/sse",
      "transport": "sse"
    }
  }
}

Common types

Place

interface Place {
  name: string;
  country: string;        // ISO 3166-1 alpha-2 ("ZA", "NG", "KE", ...)
  fclass?: string;        // GeoNames feature class — "P" = populated
  fcode?: string;         // GeoNames feature code — "PPLA" = first-order admin capital, "PPL" = generic
  population?: number;
  lat: number;
  lng: number;
  meters?: number;        // distance from query coord (reverse_geocode + nearby only)
  sim?: number;           // trigram similarity (search only)
  interesting_nearby?: Poi[];
}

Poi

interface Poi {
  name: string;
  description?: string;
  category: string;             // top-level OSM category — "tourism", "natural", "historic", "amenity"
  subcategory?: string;         // OSM subtag — "museum", "viewpoint", "place_of_worship"
  source: string;               // "osm" | "wikipedia" | "manual"
  country: string;
  lat: number;
  lng: number;
  interest_score: number;       // 0.0 – 1.0
  tags: string[];               // freeform — "tourism", "museum", "must-see", "history", "art", "nature"
  meters?: number;              // distance from query coord
}

RouteResult

interface RouteResult {
  distance_km: number;
  duration_min: number;
  mode: string;
  maneuvers: Maneuver[];
}

interface Maneuver {
  instruction: string;
  distance_km: number;
  duration_min: number;
  street_names?: string[];
}

QuestResult

interface QuestResult {
  total_distance_km?: number;     // missing if routing failed
  total_duration_min?: number;
  ordered_pois: Poi[];
  leg_count?: number;
  mode: string;
  theme?: string;
  routing_error?: string;         // present if Valhalla failed but POIs were found
  error?: string;                 // present if no POIs found at all
}

Errors

HTTP Body Cause
400 {"error_code":171,"error":"No suitable edges near location"} Routing call outside Africa (or to a point not connected to the road network)
400 {"detail":"validation error..."} Missing or invalid query param / body field
404 {"detail":"Not Found"} Wrong path. Check the spelling — /api/v2/...
429 {"detail":"rate limit exceeded"} See Rate limits — back off and retry with jitter
500 {"error":"<exception>: <message>"} Backend error. Log the body, alert ops
502 / 503 empty or HTML maps_api proxy lost contact with geo_mcp or valhalla. Retry once, then alert

Rate limits & caching

  • maps_api enforces 60 requests / minute / IP by default (slowapi) — set higher per-app via env if needed
  • Response cache (Redis, in-cluster): 24h TTL on geocode/reverse/search (place data rarely changes), 1h on routes (network conditions don't change but graph rebuilds may)
  • discover_quest is NOT cached (different POI candidate sets each call by design)
  • Always send a Cache-Control: max-age=N header from the client if you want shorter TTLs (e.g., live location apps)

Versioning policy

  • /api/v2/ is the current major version. Adding optional fields or new endpoints is non-breaking.
  • Removing a field, renaming a field, or changing a field's type is a breaking change → new /api/v3/.
  • MCP tools follow the same discipline. Tool removals or arg-rename go through a deprecation cycle of at least one minor version.
Something went wrong on this page. Reload