Skip to main content

POST /v1/match

Overview​

Map matching takes a sequence of GPS coordinates (e.g. from a vehicle tracker or fitness app) and snaps them to the most probable route on the actual road network. The result is a clean, road-aligned geometry with distance and duration estimates.

Request​

POST https://api.geocodeme.net/v1/match
Content-Type: application/json

Required headers:

  • X-Api-Key: gm_YOUR_API_KEY
  • Content-Type: application/json

Request Body​

FieldTypeRequiredDefaultDescription
coordinatesarrayYesn/aArray of [longitude, latitude] pairs (min 2, max 100 points)
timestampsarrayNon/aArray of UNIX timestamps (seconds); must match coordinates length
radiusesarrayNon/aSearch radius in meters per coordinate point (default: 5m per point)
profilestringNocarRouting profile: car, bike, foot, truck, or moto (plan-gated, see below)
geometriesstringNogeojsonOutput geometry format: geojson, polyline, or polyline6
overviewstringNofullRoute overview detail: full, simplified, or false
attributesarrayNon/aEnrich the response with per-edge segments: any of street_names, speed_limit, road_class (no extra credit cost)
use_tollsbooleanNon/aPrefer (true) or avoid (false) toll roads, motorized profiles only (400 on bike/foot)
truck_height / truck_width / truck_weightnumberNon/aVehicle dimensions (m / m / metric tons); require profile: "truck" (400 otherwise)

Cost: 1 credit, plus 1 per started block of 25 points beyond the first 25 (25 points → 1 credit, 26 → 2, 100 → 4). See Credits & quotas.

Plan gating: car is available on every plan; bike and foot require Startup or higher; truck and moto require Growth or higher. A profile outside your plan returns 403 PLAN_UPGRADE_REQUIRED, and never consumes credits. When attributes is requested, the response carries a segments array (length_meters, plus the requested attributes); if the enrichment is temporarily unavailable, segments is null and confidence is 0.0; the matched geometry itself is unaffected.

warning

Coordinates are in [longitude, latitude] order (GeoJSON standard), not [lat, lon]. This is the reverse of the /v1/geocode and /v1/reverse query parameters.

Coordinate count limits

The coordinates array must contain at least 2 and at most 100 points. The API returns an INVALID_PARAMETER error if the array is outside this range. For a single point, use /v1/nearest instead.

info

The format query parameter is not supported on this endpoint. The geometry format is controlled by the geometries request body field (geojson, polyline, or polyline6).

Code Examples​

curl -X POST \
-H "X-Api-Key: gm_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"coordinates": [
[2.3522, 48.8566],
[2.3488, 48.8534],
[2.3470, 48.8521]
],
"profile": "car",
"geometries": "geojson"
}' \
"https://api.geocodeme.net/v1/match"

Response​

info

All responses include rate limiting headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Monthly-Remaining, etc.).

With geometries=geojson (default):

{
"matched_coordinates": [
[2.3522, 48.8566],
[2.3510, 48.8555],
[2.3495, 48.8540],
[2.3488, 48.8534],
[2.3475, 48.8525],
[2.3470, 48.8521]
],
"geometry_polyline": null,
"confidence": 0.95,
"distance_meters": 620.4,
"duration_seconds": 78.2,
"attribution": "Route data (c) OpenStreetMap contributors",
"meta": {
"took_ms": 62,
"cache_hit": false
}
}

With geometries=polyline (or polyline6), the geometry is returned as an encoded string instead:

{
"matched_coordinates": null,
"geometry_polyline": "u`fiHiuqM^`BhAtEjBdI",
"confidence": 0.95,
"distance_meters": 620.4,
"duration_seconds": 78.2,
"attribution": "Route data (c) OpenStreetMap contributors",
"meta": {
"took_ms": 62,
"cache_hit": false
}
}

Schema​

FieldTypeDescription
matched_coordinatesarray | nullSnapped route as [[longitude, latitude], ...] pairs; set when geometries=geojson, null otherwise
geometry_polylinestring | nullEncoded polyline string; set when geometries=polyline or polyline6, null otherwise
confidencefloatMatch confidence between 0 and 1
distance_metersfloatTotal matched route distance in meters
duration_secondsfloatTravel duration in seconds. Estimated from the road network, or, when you send timestamps, read from them: the elapsed time between your first and last matched point, stops included
attributionstringAlways "Route data (c) OpenStreetMap contributors"
meta.took_msintegerServer processing time in milliseconds
meta.cache_hitbooleanWhether the response was served from cache
note

When overview=false, both matched_coordinates and geometry_polyline are null; only confidence, distance, and duration are returned.

Traces with gaps​

Some traces cannot be matched as one continuous drive. A U-turn is the common case: the road cannot be joined to itself in the opposite direction, so the matcher walks the trace in several pieces. Out-and-back routes and loops therefore contain at least one gap.

The response still describes the whole trace. matched_coordinates and geometry_polyline cover it from end to end, joined by a straight line across each gap, and distance_meters counts every piece. Only the segment indices carry the seam, as described below.

If you need to know where the gaps are, look for the breaks in the segment indices: a segment whose from_index is not the previous to_index starts a new piece.

Segments (attributes)​

When attributes is requested, the response carries a segments array, one entry per matched road edge:

{
"segments": [
{
"street_names": ["Quai Perrache"],
"road_class": "primary",
"length_meters": 650.0,
"speed_limit_kph": null,
"from_index": 0,
"to_index": 12
},
{
"street_names": ["A7", "E15"],
"road_class": "motorway",
"length_meters": 4300.0,
"speed_limit_kph": 130,
"from_index": 12,
"to_index": 47
}
]
}
FieldTypeDescription
length_metersfloatLength of the edge in meters
street_namesarray | nullStreet names, when street_names was requested
speed_limit_kphinteger | nullPosted speed limit, when speed_limit was requested and known
road_classstring | nullRoad classification, when road_class was requested
from_indexinteger | nullIndex of the segment's first vertex in the geometry returned by this response
to_indexinteger | nullIndex of the segment's last vertex in the same geometry

from_index and to_index locate the segment on the geometry returned in the same response, decoded. Both bounds are inclusive, so consecutive segments normally share their junction vertex: segments[i].to_index == segments[i+1].from_index. The first segment starts at 0 and the last ends at len(geometry) - 1.

The one exception is a gap (see below): where the trace could not be matched end to end, the next segment starts one vertex further on (segments[i+1].from_index == segments[i].to_index + 1). Walk the array with from_index rather than assuming the previous to_index.

Both fields are omitted when the position cannot be guaranteed

Geometry and attributes are computed by two separate engine calls. When their shapes cannot be proven identical, from_index and to_index are left out rather than approximated: an index pointing at a shape that was not served would label the wrong stretch of road while looking perfectly valid. They are also absent with overview=false, since no geometry is returned to index into.

Treat their absence as "position unknown" and fall back accordingly. Never reconstruct a position by accumulating length_meters: that value is 0 when the data is missing, so the result would be silently wrong.

Error Responses​

{
"error": {
"code": "INVALID_PARAMETER",
"message": "Parameter 'coordinates' must contain between 2 and 100 coordinate pairs",
"field": "coordinates"
}
}
tip

All endpoints are also subject to INVALID_API_KEY (401) and RATE_LIMIT_EXCEEDED / QUOTA_EXCEEDED (429) errors. See Error Codes for the full reference.