maps/docs/API.md
2026-03-30 09:22:16 +02:00

26 KiB

REST API Contract: Privacy-First Maps Application

Version: 1.0.0 Date: 2026-03-29 Base URL: Configured by user at first launch (e.g., https://maps.example.com)


General Conventions

Authentication

None. All endpoints are unauthenticated. No API keys, tokens, cookies, or sessions.

Content Types

  • JSON responses: Content-Type: application/json; charset=utf-8
  • Protobuf tile responses: Content-Type: application/x-protobuf
  • Binary download responses: Content-Type: application/octet-stream
  • Style JSON: Content-Type: application/json; charset=utf-8

Standard Error Response

All error responses use the following JSON format:

{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description of the error."
  }
}

Error Codes Reference

HTTP Status Error Code Description
400 MISSING_PARAMETER A required query parameter is missing
400 INVALID_PARAMETER A parameter value is malformed or out of range
400 INVALID_BBOX Bounding box coordinates are invalid
400 INVALID_COORDINATES Route coordinates are invalid or out of range
404 NOT_FOUND The requested resource does not exist
429 RATE_LIMITED Request rate limit exceeded
502 UPSTREAM_ERROR An upstream service (Martin/Photon/OSRM) returned an error
503 SERVICE_UNAVAILABLE An upstream service is unreachable
500 INTERNAL_ERROR An unexpected server error occurred

Common Response Headers

Header Value Applies To
Content-Type varies (see above) All responses
Cache-Control varies per endpoint Cacheable responses
X-Request-Id UUID v4 All responses (for correlation, no PII)
Access-Control-Allow-Origin * All responses
Strict-Transport-Security max-age=63072000; includeSubDomains All responses

1. Tile Serving

1.1 Get Vector Tile

Proxied to Martin tile server.

Request:

GET /tiles/{layer}/{z}/{x}/{y}.pbf

Path Parameters:

Parameter Type Required Constraints Description
layer string Yes One of: openmaptiles, terrain, hillshade Tile layer name
z integer Yes [0, 18] Zoom level
x integer Yes [0, 2^z - 1] Tile column
y integer Yes [0, 2^z - 1] Tile row

Success Response:

HTTP/1.1 200 OK
Content-Type: application/x-protobuf
Content-Encoding: gzip
Cache-Control: public, max-age=86400
ETag: "abc123"

Body: Gzip-compressed Mapbox Vector Tile (protobuf binary).

Error Responses:

Status Code Condition
400 INVALID_PARAMETER z out of [0,18], or x/y out of range for zoom level
404 NOT_FOUND No tile data exists for these coordinates
502 UPSTREAM_ERROR Martin returned an error
503 SERVICE_UNAVAILABLE Martin is unreachable

Caching behavior: Tiles are immutable between OSM data imports. The ETag changes when tile data is regenerated. Clients should use If-None-Match for conditional requests; the server returns 304 Not Modified when the tile has not changed.


1.2 Get Map Style

Request:

GET /tiles/style.json

No parameters.

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=3600
{
  "version": 8,
  "name": "Privacy Maps Default",
  "sources": {
    "openmaptiles": {
      "type": "vector",
      "tiles": ["/tiles/openmaptiles/{z}/{x}/{y}.pbf"],
      "minzoom": 0,
      "maxzoom": 14
    },
    "terrain": {
      "type": "vector",
      "tiles": ["/tiles/terrain/{z}/{x}/{y}.pbf"],
      "minzoom": 0,
      "maxzoom": 14
    }
  },
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": {
        "background-color": "#f8f4f0"
      }
    }
  ]
}

The full style document includes all layer definitions for roads, buildings, water, land use, labels, etc. The above is an abbreviated example. The mobile app uses this to configure MapLibre GL Native.

Error Responses:

Status Code Condition
502 UPSTREAM_ERROR Martin returned an error
503 SERVICE_UNAVAILABLE Martin is unreachable

2. Search / Geocoding

Proxied to Photon.

Request:

GET /api/search?q={query}&lat={lat}&lon={lon}&limit={limit}&lang={lang}&bbox={bbox}

Query Parameters:

Parameter Type Required Default Constraints Description
q string Yes Max 500 characters, control chars stripped Search query text
lat float No [-90.0, 90.0] Latitude for proximity bias
lon float No [-180.0, 180.0] Longitude for proximity bias
limit integer No 10 [1, 20] Maximum number of results
lang string No en ISO 639-1 two-letter code Preferred result language
bbox string No minLon,minLat,maxLon,maxLat (valid coordinates, min < max) Bounding box filter

Note: lat and lon must both be provided or both omitted. Providing only one returns 400 INVALID_PARAMETER.

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [4.9041, 52.3676]
      },
      "properties": {
        "osm_id": 12345678,
        "osm_type": "N",
        "name": "Vondelpark",
        "street": "Vondelpark",
        "housenumber": null,
        "postcode": "1071 AA",
        "city": "Amsterdam",
        "state": "North Holland",
        "country": "Netherlands",
        "country_code": "nl",
        "type": "park",
        "extent": [4.8580, 52.3585, 4.8820, 52.3620]
      }
    }
  ]
}

Feature properties schema:

Field Type Nullable Description
osm_id integer No OpenStreetMap element ID
osm_type string No "N" (node), "W" (way), or "R" (relation)
name string No Place name
street string Yes Street name
housenumber string Yes House number
postcode string Yes Postal code
city string Yes City name
state string Yes State or province
country string Yes Country name
country_code string Yes ISO 3166-1 alpha-2 country code
type string No Place type (e.g., "park", "house", "street", "city")
extent array Yes Bounding box [minLon, minLat, maxLon, maxLat] for area features

Error Responses:

Status Code Condition
400 MISSING_PARAMETER q parameter is missing
400 INVALID_PARAMETER lat/lon out of range, limit out of range, lang not a valid ISO 639-1 code, bbox malformed, only one of lat/lon provided
502 UPSTREAM_ERROR Photon returned an error
503 SERVICE_UNAVAILABLE Photon is unreachable

2.2 Reverse Geocoding

Proxied to Photon.

Request:

GET /api/reverse?lat={lat}&lon={lon}&limit={limit}&lang={lang}

Query Parameters:

Parameter Type Required Default Constraints Description
lat float Yes [-90.0, 90.0] Latitude
lon float Yes [-180.0, 180.0] Longitude
limit integer No 1 [1, 5] Maximum number of results
lang string No en ISO 639-1 two-letter code Preferred result language

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=3600

Response body: same GeoJSON FeatureCollection format as forward search (Section 2.1).

Error Responses:

Status Code Condition
400 MISSING_PARAMETER lat or lon missing
400 INVALID_PARAMETER lat/lon out of range, limit out of range
502 UPSTREAM_ERROR Photon returned an error
503 SERVICE_UNAVAILABLE Photon is unreachable

3. Routing

3.1 Calculate Route

Proxied to OSRM.

Request:

GET /api/route/{profile}/{coordinates}?alternatives={n}&steps={bool}&geometries={fmt}&overview={detail}&language={lang}

Path Parameters:

Parameter Type Required Constraints Description
profile string Yes One of: driving, walking, cycling Routing profile
coordinates string Yes 2-7 coordinate pairs, semicolon-separated: {lon},{lat};{lon},{lat}[;...] Route waypoints (first = origin, last = destination, middle = intermediate stops)

Coordinate constraints: Each lon in [-180, 180], each lat in [-90, 90]. Minimum 2 pairs, maximum 7 (origin + 5 waypoints + destination).

Query Parameters:

Parameter Type Required Default Constraints Description
alternatives integer No 0 [0, 3] Number of alternative routes to return
steps boolean No true true or false Include turn-by-turn steps
geometries string No geojson One of: polyline, polyline6, geojson Geometry encoding format
overview string No full One of: full, simplified, false Route geometry detail level
language string No en ISO 639-1 two-letter code Language for turn instructions

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{
  "code": "Ok",
  "routes": [
    {
      "distance": 12456.7,
      "duration": 1823.4,
      "weight": 1823.4,
      "weight_name": "routability",
      "geometry": {
        "type": "LineString",
        "coordinates": [
          [4.9041, 52.3676],
          [4.9060, 52.3680],
          [4.9100, 52.3700]
        ]
      },
      "legs": [
        {
          "distance": 12456.7,
          "duration": 1823.4,
          "summary": "Keizersgracht, Damrak",
          "steps": [
            {
              "distance": 234.5,
              "duration": 32.1,
              "geometry": {
                "type": "LineString",
                "coordinates": [
                  [4.9041, 52.3676],
                  [4.9060, 52.3680]
                ]
              },
              "maneuver": {
                "type": "turn",
                "modifier": "left",
                "location": [4.9041, 52.3676],
                "bearing_before": 90,
                "bearing_after": 0,
                "instruction": "Turn left onto Keizersgracht"
              },
              "name": "Keizersgracht",
              "mode": "driving",
              "driving_side": "right",
              "ref": "",
              "intersections": [
                {
                  "location": [4.9041, 52.3676],
                  "bearings": [0, 90, 180, 270],
                  "entry": [true, false, true, true],
                  "out": 0,
                  "in": 1
                }
              ]
            },
            {
              "distance": 0,
              "duration": 0,
              "geometry": {
                "type": "LineString",
                "coordinates": [[4.9100, 52.3700]]
              },
              "maneuver": {
                "type": "arrive",
                "modifier": null,
                "location": [4.9100, 52.3700],
                "bearing_before": 45,
                "bearing_after": 0,
                "instruction": "You have arrived at your destination"
              },
              "name": "Dam Square",
              "mode": "driving"
            }
          ]
        }
      ]
    }
  ],
  "waypoints": [
    {
      "name": "Vondelstraat",
      "location": [4.9041, 52.3676],
      "hint": "..."
    },
    {
      "name": "Dam Square",
      "location": [4.8952, 52.3732],
      "hint": "..."
    }
  ]
}

Route object schema:

Field Type Description
distance float Total route distance in meters
duration float Estimated travel time in seconds
weight float OSRM routing weight
weight_name string Weight metric name
geometry GeoJSON LineString Full route geometry
legs array of Leg One leg per pair of consecutive waypoints

Leg object schema:

Field Type Description
distance float Leg distance in meters
duration float Leg duration in seconds
summary string Human-readable summary of major roads
steps array of Step Turn-by-turn steps (if steps=true)

Step object schema:

Field Type Description
distance float Step distance in meters
duration float Step duration in seconds
geometry GeoJSON LineString Step geometry
maneuver Maneuver Maneuver details
name string Road name
mode string Travel mode for this step
ref string Road reference number (if applicable)
driving_side string "left" or "right"
intersections array Intersection details

Maneuver object schema:

Field Type Nullable Description
type string No One of: depart, arrive, turn, new name, merge, on ramp, off ramp, fork, end of road, continue, roundabout, rotary, roundabout turn, notification
modifier string Yes One of: uturn, sharp right, right, slight right, straight, slight left, left, sharp left
location [lon, lat] No Coordinate of the maneuver
bearing_before integer No Bearing before the maneuver (0-359)
bearing_after integer No Bearing after the maneuver (0-359)
instruction string No Human-readable instruction text

Waypoint object schema:

Field Type Description
name string Nearest road name to the snapped waypoint
location [lon, lat] Snapped coordinate
hint string OSRM hint for faster subsequent queries

Error Responses:

Status Code Condition
400 INVALID_PARAMETER Invalid profile, alternatives out of range, invalid geometries/overview value
400 INVALID_COORDINATES Coordinates out of range, fewer than 2 pairs, more than 7 pairs, malformed format
404 NOT_FOUND OSRM could not find a route (e.g., coordinates on unreachable islands). OSRM code: "NoRoute"
502 UPSTREAM_ERROR OSRM returned an unexpected error
503 SERVICE_UNAVAILABLE OSRM instance for the requested profile is unreachable

OSRM error code mapping:

OSRM code HTTP Status API Error Code
"Ok" 200
"NoRoute" 404 NOT_FOUND
"NoSegment" 400 INVALID_COORDINATES
"TooBig" 400 INVALID_PARAMETER
Other 502 UPSTREAM_ERROR

Caching: Route responses are NOT cached (Cache-Control: no-store). Routes depend on real-time road graph state, and caching by coordinates would have an extremely low hit rate.


4. Points of Interest

4.1 List POIs in Bounding Box

Served directly by the Rust gateway from PostGIS.

Request:

GET /api/pois?bbox={bbox}&category={categories}&limit={limit}&offset={offset}

Query Parameters:

Parameter Type Required Default Constraints Description
bbox string Yes minLon,minLat,maxLon,maxLat. Valid coordinate ranges. maxLon > minLon, maxLat > minLat. Max area: 0.25 square degrees (~25km x 25km at equator) Bounding box to query
category string No — (all categories) Comma-separated. Valid values: restaurant, cafe, shop, supermarket, pharmacy, hospital, fuel, parking, atm, public_transport, hotel, tourist_attraction, park Category filter
limit integer No 100 [1, 500] Maximum results
offset integer No 0 >= 0 Pagination offset

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=300
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [4.9041, 52.3676]
      },
      "properties": {
        "osm_id": 987654321,
        "osm_type": "N",
        "name": "Cafe de Jaren",
        "category": "cafe",
        "address": {
          "street": "Nieuwe Doelenstraat",
          "housenumber": "20",
          "postcode": "1012 CP",
          "city": "Amsterdam"
        },
        "opening_hours": "Mo-Th 09:30-01:00; Fr-Sa 09:30-02:00; Su 10:00-01:00",
        "opening_hours_parsed": {
          "is_open": true,
          "today": "09:30 - 01:00",
          "next_change": "Closes at 01:00"
        },
        "phone": "+31 20 625 5771",
        "website": "https://www.cafedejaren.nl",
        "wheelchair": "yes",
        "tags": {
          "cuisine": "dutch",
          "outdoor_seating": "yes",
          "internet_access": "wlan"
        }
      }
    }
  ],
  "metadata": {
    "total": 247,
    "limit": 100,
    "offset": 0
  }
}

Feature properties schema:

Field Type Nullable Description
osm_id integer No OpenStreetMap element ID
osm_type string No "N", "W", or "R"
name string No POI name
category string No Normalized category (one of the valid category values)
address object Yes Address details
address.street string Yes Street name
address.housenumber string Yes House number
address.postcode string Yes Postal code
address.city string Yes City name
opening_hours string Yes Raw OSM opening_hours value
opening_hours_parsed object Yes Parsed opening hours (null if opening_hours is null or unparseable)
opening_hours_parsed.is_open boolean No Whether the POI is currently open
opening_hours_parsed.today string Yes Today's hours in human-readable format
opening_hours_parsed.next_change string Yes When the open/closed status next changes
phone string Yes Phone number
website string Yes Website URL
wheelchair string Yes "yes", "no", "limited", or null
tags object Yes Additional OSM tags as key-value pairs

Pagination metadata:

Field Type Description
total integer Total number of POIs matching the query in this bbox
limit integer Limit used for this request
offset integer Offset used for this request

To paginate, increment offset by limit until offset >= total.

Error Responses:

Status Code Condition
400 MISSING_PARAMETER bbox not provided
400 INVALID_BBOX Malformed bbox, coordinates out of range, min >= max, area exceeds maximum
400 INVALID_PARAMETER Unknown category, limit out of range
503 SERVICE_UNAVAILABLE PostGIS is unreachable

4.2 Get Single POI

Request:

GET /api/pois/{osm_type}/{osm_id}

Path Parameters:

Parameter Type Required Constraints Description
osm_type string Yes One of: N, W, R OSM element type
osm_id integer Yes Positive integer OSM element ID

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=3600
{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [4.9041, 52.3676]
  },
  "properties": {
    "osm_id": 987654321,
    "osm_type": "N",
    "name": "Cafe de Jaren",
    "category": "cafe",
    "address": {
      "street": "Nieuwe Doelenstraat",
      "housenumber": "20",
      "postcode": "1012 CP",
      "city": "Amsterdam"
    },
    "opening_hours": "Mo-Th 09:30-01:00; Fr-Sa 09:30-02:00; Su 10:00-01:00",
    "opening_hours_parsed": {
      "is_open": true,
      "today": "09:30 - 01:00",
      "next_change": "Closes at 01:00"
    },
    "phone": "+31 20 625 5771",
    "website": "https://www.cafedejaren.nl",
    "wheelchair": "yes",
    "tags": {
      "cuisine": "dutch",
      "outdoor_seating": "yes",
      "internet_access": "wlan"
    }
  }
}

Same property schema as Section 4.1 features.

Error Responses:

Status Code Condition
400 INVALID_PARAMETER osm_type not one of N/W/R, osm_id not a positive integer
404 NOT_FOUND No POI found with the given type and ID
503 SERVICE_UNAVAILABLE PostGIS is unreachable

5. Offline Data Packages

5.1 List Available Regions

Request:

GET /api/offline/regions

No parameters.

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: public, max-age=3600
{
  "regions": [
    {
      "id": "amsterdam",
      "name": "Amsterdam",
      "description": "Amsterdam metropolitan area",
      "bbox": [4.7288, 52.2783, 5.0796, 52.4311],
      "size_mb": 95,
      "last_updated": "2026-03-25T00:00:00Z",
      "components": {
        "tiles_mb": 55,
        "routing_driving_mb": 12,
        "routing_walking_mb": 10,
        "routing_cycling_mb": 8,
        "pois_mb": 10
      }
    },
    {
      "id": "netherlands",
      "name": "Netherlands",
      "description": "Full country coverage",
      "bbox": [3.3316, 50.7504, 7.2275, 53.4720],
      "size_mb": 1250,
      "last_updated": "2026-03-25T00:00:00Z",
      "components": {
        "tiles_mb": 650,
        "routing_driving_mb": 200,
        "routing_walking_mb": 150,
        "routing_cycling_mb": 150,
        "pois_mb": 100
      }
    }
  ]
}

Region object schema:

Field Type Description
id string Unique region identifier (URL-safe slug)
name string Human-readable region name
description string Region description
bbox array [minLon, minLat, maxLon, maxLat]
size_mb integer Total download size in megabytes
last_updated string ISO 8601 timestamp of last data update
components object Size breakdown per component
components.tiles_mb integer Tile package size (MB)
components.routing_driving_mb integer Driving OSRM data size (MB)
components.routing_walking_mb integer Walking OSRM data size (MB)
components.routing_cycling_mb integer Cycling OSRM data size (MB)
components.pois_mb integer POI database size (MB)

Error Responses:

Status Code Condition
500 INTERNAL_ERROR Failed to read region metadata

5.2 Download Region Component

Request:

GET /api/offline/regions/{region_id}/{component}

Path Parameters:

Parameter Type Required Constraints Description
region_id string Yes Must match an id from the regions list Region identifier
component string Yes One of: tiles, routing-driving, routing-walking, routing-cycling, pois Component to download

Request Headers (optional, for resume):

Header Value Description
Range bytes={start}-{end} Request a byte range for pause/resume

Success Response (full download):

HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: 57671680
Accept-Ranges: bytes
Content-Disposition: attachment; filename="amsterdam-tiles.mbtiles"
Cache-Control: public, max-age=86400
ETag: "region-amsterdam-tiles-20260325"

Body: binary file data.

Success Response (partial / resumed download):

HTTP/1.1 206 Partial Content
Content-Type: application/octet-stream
Content-Range: bytes 10485760-57671679/57671680
Content-Length: 47185920
Accept-Ranges: bytes

Body: requested byte range.

Downloaded file formats:

Component File Format Description
tiles MBTiles (SQLite) Vector tiles for zoom levels 0-16
routing-driving Tar archive OSRM driving profile data files (.osrm.*)
routing-walking Tar archive OSRM walking profile data files
routing-cycling Tar archive OSRM cycling profile data files
pois SQLite database POI data with FTS5 search index

Error Responses:

Status Code Condition
400 INVALID_PARAMETER Invalid component name
404 NOT_FOUND Region ID not found or component not available
416 INVALID_PARAMETER Range not satisfiable
500 INTERNAL_ERROR Failed to read package file

6. Health Check

6.1 System Health

Request:

GET /api/health

No parameters.

Success Response:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
{
  "status": "ok",
  "version": "1.0.0",
  "uptime_seconds": 86421,
  "services": {
    "martin": {
      "status": "ok",
      "latency_ms": 12
    },
    "photon": {
      "status": "ok",
      "latency_ms": 45
    },
    "osrm_driving": {
      "status": "ok",
      "latency_ms": 8
    },
    "osrm_walking": {
      "status": "ok",
      "latency_ms": 7
    },
    "osrm_cycling": {
      "status": "ok",
      "latency_ms": 9
    },
    "postgres": {
      "status": "ok",
      "latency_ms": 3
    },
    "redis": {
      "status": "ok",
      "latency_ms": 1
    }
  }
}

Top-level status values:

  • "ok" — all services are healthy.
  • "degraded" — one or more services are unhealthy but the system is partially functional.
  • "down" — critical services (postgres, martin) are unreachable.

Per-service status values:

  • "ok" — service responded within timeout.
  • "degraded" — service responded but slowly (> 2x typical latency).
  • "down" — service did not respond within timeout.

Note: The health endpoint always returns 200 OK regardless of service status. The client should inspect the status field to determine overall health. This design allows load balancers to distinguish between "the gateway is running" (HTTP 200) and "upstream services are down" (status: degraded/down).

Error Responses:

Status Code Condition
500 INTERNAL_ERROR The health check itself failed (should not happen in normal operation)