Map matching: which roads a vehicle actually drove
A GPS trace is a list of points near roads, not on them. POST /trace_route and
POST /trace_attributes turn that trace into an answer about the road network:
/trace_route snaps the trace and returns a route, in the same response shape as
POST /route, while /trace_attributes returns the matched road segments and
their attributes instead. Same request body, two different questions. Both are
standard class at 0.05p per call, and a trace bills one call however many points
it carries.
What is the difference between /trace_route and /trace_attributes?
The request body is identical. The answer is not. /trace_route gives you a
journey: manoeuvres, durations, lengths and a snapped geometry you can draw.
/trace_attributes gives you the road segments themselves, so you can ask
questions about them rather than display them.
| Endpoint | Returns | Reach for it when |
|---|---|---|
POST /trace_route | The standard route response: trip.legs[].maneuvers, summary, leg shape as polyline6 | You want a clean, displayable route out of a noisy track |
POST /trace_attributes | An open JSON document: shape, matched_points, edges[] | You want to interrogate the roads that were driven |
If you are unsure which one you need, the test is whether the output goes on a
map or into a query. Rendering is /trace_route. Auditing is
/trace_attributes.
What does a map-matching request look like?
One body, either endpoint. Supply the trace as a shape array of {lat, lon}
points or as an encoded_polyline with six digits of precision, pick a
costing model, and optionally set shape_match:
export BASE=https://api.mapmap.ai
export API_KEY=snk_...
curl -fsS -X POST "$BASE/trace_route" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shape": [
{ "lat": 51.5074, "lon": -0.1278 },
{ "lat": 51.5090, "lon": -0.1300 }
],
"costing": "auto",
"shape_match": "map_snap"
}'
/trace_route returns the route response you already parse, with time in
seconds, length in the trip's units (kilometres by default) and the leg
shape as polyline6:
{
"trip": {
"locations": [
{ "lat": 51.5074, "lon": -0.1278 },
{ "lat": 51.5090, "lon": -0.1300 }
],
"legs": [{
"maneuvers": [
{ "type": 1, "instruction": "Drive north on High Street.",
"time": 240.0, "length": 0.4,
"begin_shape_index": 0, "end_shape_index": 2 }
],
"summary": { "time": 240.0, "length": 0.4 },
"shape": "}~ycbBhavgN..."
}],
"summary": { "time": 240.0, "length": 0.4 },
"status": 0,
"status_message": "Found route between points",
"units": "kilometers",
"language": "en-US"
}
}
Send the same body to /trace_attributes and you get the matched geometry plus
the per-edge attributes the engine can supply. This is an open document rather
than a fixed schema, so expect shape (polyline6), matched_points and an
edges array, and read the rest defensively:
{
"shape": "}~ycbBhavgN...",
"matched_points": [
{ "lat": 51.5074, "lon": -0.1278 },
{ "lat": 51.5090, "lon": -0.1300 }
],
"edges": [
{ "way_id": 12345, "speed": 48 }
],
"units": "kilometers"
}
A request with neither a non-empty shape nor an encoded_polyline is a 400
with type urn:sn-gateway:problem:bad-request, and the detail names the fix
verbatim:
a trace is required: supply a non-empty `shape` array or an `encoded_polyline`
The full parameter table, including filters for narrowing the attribute set on
/trace_attributes, is in the analysis API guide.
Which shape_match mode should you pick?
walk_or_snap, unless you know something the default does not. It tries the
exact edge sequence first and falls back to snapping when that fails, which is
the right behaviour for traces of unknown quality.
shape_match | Behaviour | When it is right |
|---|---|---|
edge_walk | Walks the shape as an exact edge sequence, no snapping | The trace is already known to follow real edges |
map_snap | Snaps each point to the most likely edge (hidden Markov model) | The trace is raw telematics with real noise in it |
walk_or_snap | Tries edge_walk, falls back to map_snap (the default) | You do not know which of the two you have |
costing is one of auto, truck, bus, motor_scooter, motorcycle,
bicycle or pedestrian, and costing_options reaches the engine verbatim, so
the match runs against the same costing model your planning does. Note the
billing consequence before you reach for it: "costing": "truck" moves the call
to the premium class at 1p.
What is map matching actually for?
Three jobs, in the order we get asked about them.
- Telematics cleanup. A tracker reporting every 30 seconds gives you a track
that cuts corners, wanders across dual carriageways and jumps kerbs.
/trace_routereturns it as a route with manoeuvres and a geometry that sits on the road, which is what a customer-facing journey replay needs. - Evidence of roads travelled.
/trace_attributesreturns the matchededges, so the record you keep is which segments the vehicle was on, not a cloud of coordinates that a dispute can reinterpret. This is a factual record and nothing more. It does not judge the roads, and it makes no dangerous-goods decision: see the limits below. - Toll reconciliation. On Valhalla, the default engine, matched edges carry
way_id, which you can join to your own schedule of tolled ways to check an invoice against the roads that were actually driven. Self-hosting on GraphHopper does not give youway_id, so this job needs Valhalla: see the limits below. We supply the matching, you supply the toll data. We do not price tolls and we do not pretend to.
What does map matching cost?
Map matching is standard class: one call per request, priced at 0.05p, whatever
the length of the trace. There is no per-point or per-edge metering. Unlike
/matrix, which bills one call per started block of 25 elements, and
/isochrone, which bills one call per contour, a trace request is a single unit.
| Monthly volume | Standard price per call |
|---|---|
| Included free tier | 50,000 included calls, after email verification |
| 50,001 to 1M | 0.05p |
| 1M to 10M | 0.03p |
| Beyond 10M | 0.02p |
Bands are marginal, so each price applies only to the calls that fall inside it.
The class is selected automatically per request, and it is read from the body
before the handler runs: a top-level "costing": "truck", a top-level adr
object or a top-level "hazmat": true makes the call premium at 1p. Anything
else, including every ordinary trace, is standard. Payment is prepaid credit, so
an empty balance is a 402 or a 429 rather than an invoice. Prices reconcile
with /pricing.json, which wins if this page
ever drifts.
What map matching does not do
It does not apply ADR restrictions, and you should not build a dangerous-goods
compliance step on it. /trace_route and /trace_attributes ignore a top-level
adr object if you send one, because the extension is honoured only on
POST /route and POST /optimise. The meter does not ignore it, though, and
that asymmetry will cost you: the class is read from the body, so a trace
carrying a top-level adr object or "hazmat": true bills premium at 1p and
still gets no ADR enforcement, which is 20 times the standard price for nothing.
Leave both off a trace request. Map matching tells you which roads a vehicle was
on. It does not tell you whether the load was entitled to be there. That is a
separate decision, and it has its own endpoint:
ADR tunnel compliance, enforced in costing covers
POST /adr/check.
Four more limits worth having in writing:
- The gateway rejects request bodies over 1 MiB. A long trace sent as a
shapearray of{lat, lon}objects hits that ceiling sooner than you would like. Sendencoded_polylinefor anything sizeable, or split the trace. /trace_attributesis an open document, not a contract. The attribute set depends on the engine and on yourfilters. Match on the keys you need and tolerate their absence rather than assuming a fixed shape.- Self-hosting on GraphHopper costs you the edge model. With
SN_ROUTING_ENGINE=graphhopper,/trace_attributesis best-effort: the match response is a routing path, so we returnshape,matched_pointsand instruction-derivededges, and we do not fabricateway_id,road_class,surfaceorspeed_limit. Valhalla, the default engine, has the full model. We would rather tell you the field is missing than invent it. - There is no map-matching MCP tool. Our MCP server exposes
route,matrix,geocode,check_adr_tunnelandoptimise_routes, among others, but a coding agent that wants map matching calls the HTTP endpoint directly.
Try it
The analysis API guide has the full parameter tables, the
error envelope and the shared conventions for /isochrone, /matrix,
/trace_route and /trace_attributes. A key comes from one card-free call to
POST /v1/keys, and the endpoint surface is listed in the
API reference.
Routing derives from OpenStreetMap, so credit "© OpenStreetMap contributors" when you render or republish it.
