Waze-Style Navigation at Scale: Routing Engines, Map Matching, and the Traffic Model
Building navigation at map scale — road-graph routing, map matching from GPS traces, live traffic estimation, and incremental rerouting.

Navigation looks like a shortest-path problem because the first thing you build is a shortest-path problem, and it works fine on a highway. It fails in a city, it fails in a tunnel, and it fails at 5pm on a Tuesday. The reasons are a road graph problem, a signal-processing problem, and a statistics problem, in that order, and none of them is the one you expect.
The scale
a country-scale navigation service
5M km of road network
40M road segments (nodes and edges)
2M nodes
1,000 concurrent users
- 1 route request every 10s
- 1 location update every 1s per user
peak: 50,000 users
- 5,000 route requests/s
- 50,000 location updates/s
and every user generates
- continuous GPS at 1Hz
- a reroute every 20-60s as the
ETA drifts
50,000 location updates a second is the number that shapes the architecture, because each one is not a point — it is a question about which road the user is on, and answering it wrong poisons everything downstream.
The three problems
+----------------------------------------------------------+
| 3. TRAFFIC MODEL |
| speed on a road at a time, from probe traces |
| "how fast is this road right now" |
+----------------------------+-----------------------------+
|
v
+----------------------------------------------------------+
| 2. ROUTING |
| shortest path on a directed graph with time-dependent |
| weights |
| "given current speeds, what is the best path" |
+----------------------------+-----------------------------+
|
v
+----------------------------------------------------------+
| 1. MAP MATCHING |
| GPS trace -> position on the road graph |
| "which road is this device on, and how far along it" |
+------------------------------------------------------------+
Each layer’s output is the next layer’s input, and each has a characteristic failure. Map matching fails and the route thinks you are on a parallel street. Routing fails and it sends you down a road that is closed. The traffic model fails and the ETA is consistently wrong in one direction at rush hour.
Map matching
raw input
1Hz GPS, 5-15m accuracy
10 points, so a ~100m trail
the problem
which edges of the road graph does
this trail correspond to, in order?
why it is non-trivial
- urban canyons: GPS reflects off
buildings, a 15m error becomes 100m
- parallel roads 20m apart: a
surface street and an elevated
highway, both plausible
- underpasses and tunnels: no signal
for 200-800m, and the position
jumps on exit
- one-way streets: matching onto a
one-way in the wrong direction is
a common error
- U-turns made legitimately at a
median look like a GPS failure
the algorithm that works: HMM
states = candidate positions on
nearby road segments
emission = GPS distance to that
candidate position
transition = distance travelled
between candidates
(drives the state
continuity)
output = Viterbi decode, with
backtracking to recover the
full path
the transition term is what does the
work: a candidate 200m away is not
reachable in 1 second at any legal
speed, so it is eliminated
In the common HMM formulation, candidate road positions are hidden states. The emission probability models how plausible a GPS observation is for a candidate, commonly from distance/error. The transition probability compares network travel between candidates with observed displacement and elapsed time. Candidate generation and reachability pruning keep the state space bounded; they are not the HMM prior itself.
Two details that separate production matchers from textbook ones:
Candidate and transition bounds. Generate nearby road candidates spatially, then reject transitions whose network distance, direction, or implied speed is implausible. A Euclidean disc alone cannot see barriers, turn restrictions, or two parallel carriageways.
Tunnel handling. When the signal drops, the matcher should coast on dead reckoning — position plus heading plus speed — and treat the first post-tunnel fix as a fresh, low-confidence observation rather than a hard constraint. A matcher that demands continuity across a 400m gap will snap the device to the wrong exit when it reappears, which is one of the most visible navigation failures there is.
The routing engine
road graph
nodes = intersections, turn points
edges = road segments, directed
weight = travel time = length / speed
with time-dependence from
the traffic model
the baseline problem
2M nodes, 40M edges
a naive Dijkstra explores a large
fraction of the graph: ~200ms+ and
huge memory per query
the fix: contraction hierarchies
1. PREPROCESS (offline, ~10 min)
- add shortcut edges over
high-importance nodes
- rank nodes by importance
- repeatedly contract the least
important node, adding shortcuts
2. QUERY
- bidirectional upward search
from source and target
- a few hundred node expansions
total
- ~0.5ms per query
the shape of the win
preprocessing: expensive, rare
query: cheap, constant-ish
-> the whole design bets that
queries vastly outnumber graph
changes
Contraction hierarchies precompute shortcuts so a query searches upward from both ends. Topology changes such as a new road affect preprocessing, but weight changes are not free in a classic hierarchy either: shortcut weights must remain valid for the metric. Customizable Route Planning and Customizable Contraction Hierarchies separate topology preprocessing from a faster metric-customization phase, which is the relevant design for live traffic and user-specific costs.
The alternative family is ALT (A*, Landmarks, and Triangles), which precomputes a small set of landmark distances per node to make A* admissible and much more directed. ALT has better local updates than CH; CH has a much smaller query cost. Most production engines use CH for the fast path and accept the rebuild cost, batching graph changes.
Turn costs matter more than distance. A turn from a motorway onto a service road is not a simple edge traversal — it has a time penalty that depends on the turn type and the traffic light. Ignoring turn restrictions and turn penalties is the most common cause of “why did it send me down that impossible turn”.
The traffic model
probe data
phones reporting GPS
20M users x 1Hz x a few hours
of driving each = billions of
matched points per day
the pipeline
1. map-match the traces (same
matcher as navigation)
2. segment them into runs of
consistent direction
3. attribute speed to (edge, time
bucket, day-of-week)
4. aggregate, smooth, and
sanity-filter
the statistical problem
a single trace on a road at a time
tells you very little
- one slow drive might be a traffic
light, a truck, or a phone in a cup
holder
- you need many traces, and a
robust estimator
one estimator contract
for each (edge, 15-min bucket,
weekday/weekend):
collect speeds
estimate a travel-time distribution
- robust central estimate for ETA
- upper travel-time percentile for
a stated on-time reliability target
- confidence from probe count/age
with a prior from historical data
for low-traffic edges
A high percentile of speed selects the faster observations and tends to underestimate travel time; it is not a general ETA estimator. For reliability, transportation practice commonly reports an upper percentile of travel time—for example the 95th-percentile planning time—not a high speed percentile. A traffic-light distribution may be multimodal, so retain time-of-day/signal context or estimate the travel-time distribution. Publish whether the route ETA is expected, median, or reliability-buffered; each answers a different product question.
Coverage is the other half. Most edges have no probes — side streets, rural roads, anything at 3am. The model needs a prior from historical and road-class-based defaults, blended with observations where they exist, and it needs to be explicit about which is which, because a road with three observations should not be trusted as much as one with thirty thousand.
Incremental rerouting
the naive approach
every 30 seconds:
recompute the full route from
the user's CURRENT position to
the destination
why it is wrong
- the "current position" is on the
matched road, which can be a
wrong guess; a full recompute
propagates that guess into a
confident wrong route
- the user gets a "recalculating"
that flips the route for a
200m improvement
- it is far more computation than
necessary
the correct approach
1. keep the matched position as
(edge, offset, direction)
2. recompute FORWARD from that node
using an A* / bidirectional search
bounded by the current remaining
distance
3. only replace the route if the new
one is better by a margin
(e.g. > 30s or > 2%)
4. the "rerouting" announcement is
gated on the same margin, so
the user is not told about
improvements they do not care about
The margin is the detail that decides whether the app feels good. Without it, a nav app recalculates every twenty seconds and the route line visibly jitters; with a 30-second or 2% margin, the route only changes when it genuinely should, and the occasional recalculation reads as “it noticed the traffic changed” rather than “it is broken”.
Forward recomputation also needs the destination to be reached robustly when the current position is wrong. The safe pattern is to keep a short “committed” prefix of the previous route — the next few hundred metres along the currently-matched road — and search from the end of that. This prevents the classic flip where a GPS error on a service road makes the app turn around and rejoin the main road behind you.
Failure stories worth testing
Drive a route with the GPS in a car park with a 200m reflection
Confirm the matcher does not snap to the wrong parallel street, and that turn restrictions are respected on the matched road.
Tunnel for 400m with a dead-reckoned position
The first fix after the tunnel must be low-confidence. If it snaps to the nearest road, it will pick the wrong exit.
Add a new road and time the rebuild
This is the constraint that decides whether you can use contraction hierarchies at all. If the rebuild is 10 minutes, all map changes must be batched.
Time 10,000 route queries per second
The query path should be sub-millisecond and the answer is whether the hierarchy is doing its job.
Remove all turn penalties and rerun the city benchmark
Routes will include illegal turns and counterflow violations. This is the most common cause of a “the map is wrong” bug report.
Compare mean/median travel time and a reliability percentile on a signalized road
The ETA becomes wrong in both directions and users describe it as “it thinks every road is a traffic jam”.
Remove the reroute margin and drive a 30-minute route
The route line jitters continuously and users report the app as unreliable. This is the single most important UX parameter in the rerouting loop.
Make an offline map and reroute in a dead zone
Fallback behaviour: dead reckoning on the last matched edge plus a coarse route. Confirm the app degrades to a usable state instead of freezing.
Confine a user to one side of a dual carriageway with a fence line
Test whether the matcher respects the fence line and whether U-turns are only allowed at legal points. Fence lines are part of the graph and are frequently forgotten.
Add a high-frequency bus lane that is only open at certain times
Time-dependent edges are needed, and the accessibility rules have to come from the routing profile, not the base graph.
Set the probe threshold too low and include stationary GPS
Stationary traces at a red light will drag the distribution. Test with and without a movement filter.
Run a query from a node that is not in the hierarchy because it was added late
This is the classic “works in tests, fails for new roads” bug. Every new node must be contracted into the hierarchy.
Make the destination unreachable and check the messaging
The app should say it cannot find a route, not silently draw a line to the nearest reachable point.
A production-ready architecture
OS location (1Hz)
|
v
+----------------------------------------------------------+
| MAP MATCHER |
| HMM over road segments |
| - reachable-set filtering (roads only) |
| - transition term eliminates impossible candidates |
| - dead reckoning through tunnels |
| - confidence score per matched position |
+----------------------------+-----------------------------+
|
v
+----------------------------------------------------------+
| ROUTE ENGINE (contraction hierarchy) |
| - preprocessed shortcuts, bidirectional upward search |
| - time-dependent weights from the traffic model |
| - turn costs and restrictions from the profile |
| - forward-only search for reroutes |
+----------------------------+-----------------------------+
|
v
+----------------------------------------------------------+
| TRAFFIC MODEL |
| - probe traces -> matched runs -> (edge, bucket, day) |
| - p85/p90 estimator, not mean |
| - prior blended with observations for low coverage |
| - per-edge confidence |
+----------------------------+-----------------------------+
|
v
+----------------------------------------------------------+
| ROUTE GUIDANCE |
| - reroute only if better by a margin (30s or 2%) |
| - committed prefix to survive position errors |
| - offline fallback: dead reckoning + coarse route |
+----------------------------+-----------------------------+
watch: match confidence, match error (vs ground truth),
route recompute rate, reroute flip rate, ETA error
by time of day, edge coverage
Delivery checklist:
- Build the matcher before the router. Everything downstream inherits the matcher’s mistakes, and a route over a wrongly matched position is confidently wrong.
- Use an HMM with a transition term that reflects real travel speed; it is what eliminates impossible candidates in urban canyons.
- Handle tunnels by dead reckoning and treating the first post-tunnel fix as low confidence.
- Use contraction hierarchies for the query path and batch all graph changes to fit the preprocessing budget.
- Never encode live traffic as new edges. Time-dependent weights on the existing graph, or you will be re-preprocessing all day.
- Include turn restrictions and turn costs in the graph, not in the UI layer.
- Use a high percentile of observed speed per time bucket, not a mean, and blend a prior into low-coverage edges.
- Recompute reroutes forward from the matched position with a small committed prefix, never from scratch.
- Gate reroute announcements on a margin so the route does not visibly jitter.
- Filter out stationary and low-speed-because-parked traces from the traffic estimator.
- Respect fence lines and legal U-turn points in the graph.
- Carry a confidence score on the matched position and degrade gracefully on low confidence.
- Make the offline mode a real degraded state (dead reckoning plus a coarse route) rather than a freeze.
- Measure match error against ground truth in a test city; without it, map matching regressions are invisible.
Common mistakes
| Mistake | What actually happens | Better decision |
|---|---|---|
| Routing before matching is solid | Confidently wrong routes from bad positions | Matcher first, measure error |
| Mean speed for traffic | Wrong in both directions on signalised roads | p85/p90 of the distribution |
| No transition term in matching | Parallel streets and one-ways mis-matched | HMM with reachability constraints |
| Tunnel treated as a hard constraint | Snaps to the wrong exit | Dead reckon, low-confidence re-entry |
| Rebuild the hierarchy per map change | Never keeps up with traffic events | Time-dependent weights, batched rebuilds |
| No turn costs or restrictions | Illegal turns in the route | Turn costs in the graph |
| Reroute on every ETA change | Route jitters, app feels broken | Reroute margin, committed prefix |
| Reroute from scratch each time | GPS error propagates into a wrong route | Forward search from matched node |
| No stationary-trace filter | Traffic lights dominate the model | Movement filter before aggregation |
| Low-coverage edges trusted fully | Rural ETAs wildly wrong | Prior blended with observations |
| Fence lines missing | U-turns through medians | Fence lines and legal U-turns in graph |
| No offline degraded path | App freezes in a dead zone | Dead reckoning plus coarse route |
| No match-confidence carried | Low-confidence fixes trusted fully | Confidence score, degrade on it |
| New nodes not contracted into hierarchy | Works in tests, fails on new roads | Contraction as part of the build |
| No ground-truth match measurement | Regressions invisible | Test city with known routes |
The complete story in one minute
Navigation is three problems stacked. Map matching uses candidate road positions, an observation-error model, and network-aware transition likelihoods; tunnels need an uncertainty-growing dead-reckoning state and cautious re-entry. Routing can use contraction hierarchies, but live metrics require shortcut customization or a dynamic-routing design, not arbitrary weight replacement in a static hierarchy. Traffic estimation must distinguish expected ETA from reliability: a high speed percentile is optimistic, while an upper travel-time percentile is a buffer. State the estimator contract or the same model will be judged against incompatible meanings of “arrival time.”
Rerouting is what makes the app feel good or broken, and it is not a full recompute. Recompute forward from the matched node with a short committed prefix, and gate the change on a margin — around 30 seconds or 2%. Without that margin the route line visibly jitters every twenty seconds and users conclude the app is unreliable; with it, the occasional recalculation reads as “it noticed the traffic”. Measure map-matching error against ground truth in a test city, because without that metric every matching regression is invisible.
matcher first (HMM + reachability + dead reckoning), then router
contraction hierarchies, time-dependent weights, no per-traffic rebuild
p85 speeds per bucket with a prior, stationary traces filtered
forward reroute with a committed prefix and a change margin
The hard part was never only the shortest path. It was carrying uncertainty from map matching into a metric that can be customized safely, then telling the user whether the ETA is expected or reliability-buffered.


