The Road Your GPS Thinks It Is On
On a Tuesday morning in July, a phone riding in a jacket pocket recorded a drive along Unter den Linden in Berlin and kept three points: 52.5164, 13.3777 at the Brandenburg Gate; 52.5170, 13.3900 roughly 830 m east; 52.5175, 13.3950, another 340 m on. Three dots over Mitte. Anyone who knows Berlin can name the road from that description. The trace itself cannot, because raw positions carry noise and nothing else, and the street only exists in the answer if you put it there.
Putting it there is called map matching, and it is the step that turns a breadcrumb trail into a route. Get it right and every downstream consumer of the trace stops guessing: speed estimates attach to streets, and turn-by-turn replays become drives a vehicle could legally have made. Get it wrong and the trace will quietly assert a road the vehicle never touched. This tutorial runs one trace through the whole chain, from three raw dots to a matched road, and gives equal time to where the chain breaks.
Three dots that cannot name their own street
# the pocket GPS log, exactly as recorded
52.5164,13.3777
52.5170,13.3900
52.5175,13.3950
Plot the three points, connect them with lines, and the drawing looks like a trip: about 830 m of movement, then about 340 m more, heading east out of Mitte. A drawing is all it is. Between recorded points sits the better part of a minute of driving that the log never saw, and the line through that gap is an interpolation, not a record of anything.
Downstream, a drawing fails the questions people actually bring to a trace. Which street was that segment on? A road authority counting traffic per street, or a courier paid by the kilometer outside the city, needs street identity, and a coordinate carries none. Was the vehicle moving east? The line implies it; the data never said it. Was every drawn segment a legal drive? Only the road graph can answer that, because on other traces in other cities the drawn line will cross plazas and plinths where no vehicle is allowed, and nothing in the coordinates will warn you.
The road is a hidden state, and the trace is the evidence
Map matching has a clean mental model, and it pays to hold before running anything. For every recorded point, list the candidate roads: the streets within some radius of the point, on the order of tens of meters, where the point could plausibly sit once noise is accounted for. At the first point near the Brandenburg Gate, the honest candidate list is several roads long: Unter den Linden running east, Straße des 17. Juni running west toward the Tiergarten tunnel, and Behrenstraße, one parallel block south.
Transitions score the pairs. The surviving pair is the one where the road-graph path between two candidates has a length close to the straight-line gap between the observed points. Chains that require teleporting across the Spree or u-turning against the graph score badly, and the matcher walks the whole sequence at once, keeping the chain of candidate roads that best explains the trace from end to end. The formal versions of exactly this idea are standard, hidden Markov models decoded with Viterbi, and every serious routing platform you already use contains some version of it.
The reason matching works at all is that roads constrain motion far more tightly than GPS noise can smear it. Noise here is measured in meters. The alternatives are streets apart. On our trace, the second point at 52.5170, 13.3900 could belong to Unter den Linden or to Dorotheenstraße a block or two north. The third point continues east with the drift pattern of the boulevard, and the road-graph distances line up for exactly one of the two. GPS in a pocket is far noisier than most people assume. What rescues the trace is the ruthlessness of the road geometry, which the matcher borrows.
Running the trace through matching
everymap --api-key $KEY match-route --trace "52.5164,13.3777;52.5170,13.3900;52.5175,13.3950" --transport car
That single command is the whole pipeline. The everymap CLI, from the everymap-rs project we publish under MIT, takes the trace, requests candidates and transitions from the provider, and returns the matched path on the road graph. In this run the matched line follows Unter den Linden, running east from Pariser Platz, with the road identity carried in the geometry itself. Reverse-geocode the matched line if you need the street name as a label for humans.
Be honest about what this example is, because it is the easy case. Three points, well spaced, already sitting close to the boulevard, with street alternatives a full block apart: the matcher wins because the geometry agrees with itself. Pocket recording really does produce gaps like these, about 830 m and 340 m in this log, which means the entire stretch between dots was inference. It worked because the roads are far apart. The failures start exactly where the alternatives are close.
Matching loses exactly where roads run close together
Where a road runs as two one-way halves a few meters apart, both halves sit in the candidate list with nearly identical scores. The matcher picks one, and every median gap the trace crosses can flip the verdict, so the reconstructed turn list sprouts u-turns the driver never made.
Frontage roads and motorway ramps duplicate the geometry of the carriageway they serve. A trace sampled every few seconds cannot reliably tell a shoulder from a ramp, and the matcher, handed candidates that close, will sometimes report the ramp.
In a dense grid, a Manhattan block or a Kreuzberg one, a single point has a dozen candidates at near-equal distance, and heading noise flips the match street by street. The failure looks like a driver sliding sideways through the block plan rather than obeying corners.
Tunnels deserve their own paragraph. West of the Brandenburg Gate, the Tiergarten tunnel carries the road under the park and swallows the receiver for more than a kilometer. The trace goes silent, then reappears on the far side. Over the gap the matcher must choose between the tunnel road, which is where the vehicle almost certainly was, and the surface network, which offers many plausible candidate chains for the same displacement. The surface route can win on raw score, because it is built entirely from real roads while the tunnel contributes a confident guess at each end only. The honest output here is "most likely the tunnel, held with low confidence", and not every engine offers the low-confidence half.
Working with a matcher instead of against it
Sample faster than your failure mode. A point every few seconds disambiguates most of the list above without any new modeling, because transitions get denser evidence to work with.
Feed heading and speed when the device has them. The transition scoring consumes both, and a heading pins the direction of travel on any road that carries two ways.
Ask for confidence, and route the low-confidence stretches to a review bucket rather than straight to your dashboard. A confidence field you ignore costs nothing; a match you trusted blindly in a tunnel costs an investigation later.
Accept the structural limit: a matcher can only choose among the roads in the graph it was given. When a match comes back badly wrong, the algorithm is usually innocent and the graph is the suspect: the service road missing, the one-way flag stale, the parallel ramp never digitized. Fix the data before you tune the decoder, or you will spend a season calibrating a model against a map that is lying.
The road your GPS thinks it is on is an inference, complete with a confidence score that many engines treat as optional. When the inference fails, read the failure rather than discarding it. It names the exact place where the map and the world parted ways.
Thanks for reading this far. If this sounds like the problem you're wrestling with, tell us about it. A principal reads every message.
Start a conversation