Skip to content
GuidesBlogPlaygroundDashboard
Routing & Navigation

Map matching

Snap noisy GPS traces to the road network. Clean up fleet tracks, activity recordings, and raw sensor data.

GPS points drift into buildings, jump across rivers, and wander off roads. Map matching snaps a raw trace to the most likely path on the road network using a Hidden Markov Model, returning snapped tracepoints with matched road segments.

Send the trace as a GeoJSON LineString in the geometry field. Coordinates are [longitude, latitude] pairs. Optional radiuses controls the search radius per point (meters, default 50):

Terminal window
curl -X POST https://plaza.fyi/api/v1/map-match \
-H "x-api-key: pk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "LineString",
"coordinates": [
[13.3888, 52.5170],
[13.3910, 52.5168],
[13.3935, 52.5172],
[13.3961, 52.5165],
[13.3990, 52.5161]
]
}
}'
import Plaza from "@plazafyi/sdk";
const client = new Plaza();
const matched = await client.v1.mapMatch({
geometry: {
type: "LineString",
coordinates: [
[13.3888, 52.517],
[13.391, 52.5168],
[13.3935, 52.5172],
[13.3961, 52.5165],
[13.399, 52.5161],
],
},
});
import plaza
client = plaza.Client()
matched = client.v1.map_match(
geometry={
"type": "LineString",
"coordinates": [
[13.3888, 52.5170],
[13.3910, 52.5168],
[13.3935, 52.5172],
[13.3961, 52.5165],
[13.3990, 52.5161],
],
},
)
import "github.com/plazafyi/plaza-go"
client := plaza.NewClient()
matched, _ := client.V1.MapMatch(ctx, plaza.MapMatchParams{
Geometry: plaza.GeoJSONLineString{
Type: "LineString",
Coordinates: [][]float64{
{13.3888, 52.5170},
{13.3910, 52.5168},
{13.3935, 52.5172},
{13.3961, 52.5165},
{13.3990, 52.5161},
},
},
})

Response — a GeoJSON FeatureCollection. Each feature is a snapped tracepoint; the top-level matchings array contains matched road segments.

{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [13.3889, 52.5170] },
"properties": {
"original": [13.3888, 52.5170],
"distance_m": 1.2,
"edge_id": 48201,
"name": "Unter den Linden",
"matchings_index": 0,
"waypoint_index": 0
}
}
],
"matchings": [
{ "distance": 720, "geometry": { "type": "LineString", "coordinates": "..." } }
]
}
FieldWhat it means
originalThe original [lng, lat] coordinate you sent.
distance_mDistance from the original point to the snapped location in meters.
edge_idThe road edge the point was snapped to.
nameStreet name of the matched edge (when available).
matchings_indexIndex into the top-level matchings array for this point’s matched sub-route.
waypoint_indexThis point’s index in the original input.

Control how far each GPS point searches for candidate roads:

{
"geometry": {
"type": "LineString",
"coordinates": [
[13.3888, 52.5170],
[13.3910, 52.5168],
[13.3935, 52.5172]
]
},
"radiuses": [100, 50, 75]
}

Each value is a search radius in meters for the corresponding coordinate (default: 50m).

Point density matters. One point every few seconds works well. One per minute produces poor matches — too much ambiguity between points.

Trim cold-start junk. GPS receivers produce noise for the first few seconds. Cut scattered initial points before sending.

Coordinate limit. Max 50 points per request. For longer traces, split into overlapping segments and stitch.

Fleet tracking. Snap delivery vehicle traces to actual roads for accurate distance and replay.

Activity recording. Clean up running/cycling GPS tracks that zigzag through buildings.

GPS cleanup before analysis. Map match probe data before travel time or speed studies — raw noise pollutes results.