AngoraTek FR
← Back to Blog
Field Notes 10 min read

Why Routing Engines Disagree

Take one Berlin trip, from 52.52, 13.40 to 52.54, 13.42, and hand it to four routing engines. Radar, the fifth provider our client library covers, models four of the ten domains and stays out of this routing comparison. HERE, Google, TomTom and Mapbox each answer in well under a second, and each answer is confident: a polyline, an ETA, a distance, a turn list. On the days we have run this exact pair, the four responses rarely agree on more than the skeleton of the journey: the origin street, the fastest main corridor, and a place to get off it. The row below records one of those captures; we do not average the deltas into a number we would defend forever, because the day you benchmark on changes the answer.

The disagreement is not noise, and it is not a bug. A route is the minimum of somebody's cost function, computed over somebody's private snapshot of the road network, sold under somebody's business model. Those snapshots, cost functions and business models were each tuned for a different customer. We have stopped reading the spread between engines as an error term, because it carries information no single response contains: it tells you who each engine believes is asking. A dispatcher, a truck driver and a cyclist are not asking the same question, and no single engine answers all three of them well.

Four engines, one pair of coordinates, four opinions

Nothing about the query changes from one line to the next.

# one query, two providers shown; the other two behave the same
everymap --api-key $HERE_KEY route --origin "52.52,13.40" --destination "52.54,13.42" --transport car
everymap --provider google --api-key $GOOGLE_KEY route --origin "52.52,13.40" --destination "52.54,13.42" --transport car

Both commands come back in roughly the same shape, which is the point of the client that runs them. everymap is a Rust wrapper we built and publish under MIT, and it exists so that the comparison above is two lines instead of two integrations. Identical inputs make any difference in the outputs a finding about the engines rather than about our code. The duration fields in the two responses have never matched on any day we have run this pair, by seconds on clear mornings and by minutes when one map knows about a closure the other has not ingested. Nothing in either response tells you which engine is closer to the truth on your street, on your day.

Every field came from a decision made upstream of the request. There are four upstream decisions doing almost all of the work, and each one deserves its own look.

The map is a bet, not a photograph

Each provider maintains its own picture of the road network, assembled from paid surveys, government releases, commercial feeds, fleet data and, in at least one case, years of anonymized phone probes. The picture is patched continuously, but it is patched where problems were noticed. Two providers can hold the same city and hold it differently. A turn restriction missed by one changes the cost of every route crossing that junction. A road classed as residential in one graph and service in another quietly reshapes which streets are worth entering.

The consequences reach the ground fast. The exit ramp that reopened this spring can sit in one provider's graph by late summer and still be missing from another's this October. A delivery scheduler running on the older map then sends half its trucks around a building that no longer requires a detour. Both engines report confident routes. Neither is lying. One of them bet differently about which edits mattered, and the bet expired without anyone being told.

The same bet runs the length of the stack, down to places that never move: ask four engines where Brandenburg Gate is, and interpolation differences can put the pin tens of meters apart.

# one address, four answerers
everymap --api-key $HERE_KEY geocode "Brandenburg Gate, Berlin"
everymap --provider google --api-key $GOOGLE_KEY geocode "Brandenburg Gate, Berlin"

A route is the minimum of somebody's cost function

Give any engine the same origin, the same destination and three transport modes, and you will get three different philosophies of the road. The car profile mostly buys time. The truck profile must obey the road as written for a 13.6 m trailer: clearances, axle limits, weight restrictions, hazmat classes, so its cheapest path is legal-first and fast-second. The EV profile weighs gradients, because a hill is a battery decision, and it will budget a charge stop into a route a petrol car would never think about.

The subtler disagreement is between two engines in the same mode. One weights a left turn across traffic like a tax and will drive three extra blocks to avoid it; another barely counts it. One charges a heavy penalty per traffic signal, another folds signals into an average intersection delay measured years ago on different streets. Same origin, same destination, different roads, and neither engine made a mistake. Each optimized the question it was handed. The user who was not consulted is the one holding the phone.

Traffic models disagree because they measure different vehicles

The congestion layer is where engines drift furthest apart, because the physical sources differ. Google's live speeds come overwhelmingly from consumer phones running its software, which makes its coverage in cities extraordinary and its coverage of a forestry road thin at best. TomTom and HERE fuse probe feeds with historical speed profiles, the kind of model that can describe what a Tuesday at 08:00 usually looks like even when nobody is transmitting today. Mapbox builds its estimates from anonymized traces flowing through the SDKs its customers ship.

Hold those models against the same road and the disagreement arrives in a pattern rather than at random. A motorway instrumented with loop detectors and dense with commuters gets measured well by every source, and the four ETAs cluster. A residential cut-through barely registers in the commercial feeds, and the engines diverge. The practical consequence: an engine can be the best available on Sunday morning and the worst available on Monday morning, on the same streets, because its model of Tuesday morning was calibrated on somebody else's Tuesdays.

The freshest layer is also a pricing decision

Update cadence is the quiet multiplier. Each provider refreshes its graph, its traffic layer and its historical profiles on its own schedule. Some recompute live traffic every few minutes, some ship profile updates on slower cycles, and the API you query can reflect a graph months old while the consumer app from the same company shows this morning. The cadence is not published loudly, and it is not uniform across products from the same vendor.

Pricing shapes the answer too. Engines priced per transaction have reason to keep you querying rather than precomputing; some of the most valuable attributes, the truck dimensions and the turn-level legal restrictions, sit behind higher contract tiers. Naming no numbers that change monthly: the map you receive is partly a function of the invoice you agreed to. When a provider retires its pricing, your routes can change without a single line of your code moving.

We spent two weeks comparing screenshots

The first version of this comparison, ours, was a dead end and it has earned its honest two sentences. In spring 2026, before the CLI was published, we compared engines by running the same queries side by side and eyeballing responses formatted four different ways, alongside whatever forum threads we could find. That produced strong opinions and no data. Every disagreement in an ETA had three candidate explanations, a newer graph, a different turn penalty, a traffic model calibrated on another day, and eyeballing cannot separate them.

The fix was not more data; it was fixing the inputs. That is the whole reason everymap grew a benchmark harness in v0.2.2, first published September 15, 2026. everymap-bench runs the same origin-destination pairs through every configured provider, with the same transport mode, and emits table, JSON or markdown output, so a disagreement becomes a row in a file instead of a screenshot in a thread. It does not make the providers absolutely comparable. It makes their disagreement repeatable, which is the part you can act on.

What a controlled comparison actually is

Three things get held constant, and all three are cheap to hold constant once the plumbing is one client.

The pairs come from the city you operate in, not from a vendor's demo address. A benchmark of Brandenburg Gate to Potsdamer Platz tells a Berlin logistics operator something; it tells a Toulouse courier almost nothing.

The day and the time windows are fixed and recorded, because Tuesday 07:30 is a different map from Sunday 11:00.

One scoring rule per domain, decided before the run: distance and turn count for a dispatch system, arrival plausibility against a fixed schedule for a shuttle service, charge-stop count for EV range planning.

pair     origin       destination    transport   captured
ber-001  52.52,13.40  52.54,13.42    car         2026-09-08 07:30 CEST

The honest limit is that traffic cannot be replayed. A crash or a football match shifts the roads by more than most model differences, and none of it can be held constant between runs. One captured day is a claim about one captured day. Run the pairs repeatedly when the decision is expensive, and accept that a benchmark answers "who is better today" with far more confidence than "who is better".

The disagreement is a map of your users

Once the spread is measured, it becomes a selector. The table below is our triage, formed on the pairs we run. It is opinion with a source, not a vendor-neutral evaluation, and it should be re-run at contract renewal.

The question being askedThe engine that tends to winWhy it tends to winWhat you pay for it
Truck routing with clearance and axle rulesHEREdeepest road-attribute layering of the fourthin consumer tooling wrapped around the data
Consumer-grade live traffic in dense citiesGooglephone probe coverage no road sensor network matchesno visibility into, or control of, the model
EV range and charge-stop planningTomTomcharging and range attributes sit close to the routernarrower developer ecosystem
Custom cost profiles in your own codeMapboxthe router is built to be reweighted by developerstraffic estimates you cannot inspect
The reason the table is a map of users rather than a ranking of engines is that the same origin and destination get scored three different ways by three different people. A dispatcher scores it by fleet minutes. A driver scores it by certainty of arrival. A cyclist scores it by whether the route ever feeds them into four lanes of traffic. Those three will often be served best by three different engines, and once the questions are written down properly a single-provider "best" does not exist. The mistake is collapsing all three users onto one contract because one dashboard is easier to read.

Where this reasoning stops

The limits need as much space as the wins. Our benchmark pairs number in the dozens, not the thousands, so the harness answers triage questions and is not a leaderboard. The engines change their graphs and their contracts beneath the benchmark, so any conclusion carries today's date and expires at renegotiation. And scoring rules are opinions with units attached; the moment your users' real cost differs from the rule, re-derive the rule from the user rather than from the benchmark.

Four engines disagreeing about the same two points are not four failures. They are four definitions of "best", each with a customer attached, and the job is to figure out precisely who is asking.

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