DEV Community

Wouter van Kemenade
Wouter van Kemenade

Posted on Originally published at pack-lightly.com

Why Route Ranking Needs Deterministic Scoring, Not a Language Model

When you build a tool that compares travel routes: flight vs. train vs. bus, the obvious instinct is to let a language model read the variables and pick the best one. Cost per person, door-to-door time, comfort, CO2, frequency, group pricing, all fed into one prompt.

That approach breaks down fast at scale.

The LLM call problem

A model excels at explaining why a train beats a bus on one journey. It fails at consistent, reproducible ordering across 146 countries and thousands of route combinations.

Cost: rank five options per route across 7,000 routes and you're paying for millions of inference tokens to render a page. Consistency: the same route, queried twice, can return a different order because the model's read of "comfort" shifted between runs. Users refresh a bookmarked page and the ranking moves, which reads as a bug, not a feature. Latency: an API call per comparison turns a 200ms render into a round trip with retries and backoff.

The deterministic alternative

A scoring algorithm just weights the inputs:

score = (cost_weight × cost_normalized) + (time_weight × time_normalized) + (comfort_weight × comfort_normalized) + (co2_weight × co2_normalized)
Enter fullscreen mode Exit fullscreen mode

Travel style, budget-first, comfort-first, middle ground, sets the weights. Rank by score. Same calculation every time, on every device, for every route. Change the travel style and the ranking recalculates client side, instantly, with no API call and no variance.

What deterministic costs you

The tradeoff is maintenance, not documentation. Every route needs real cost per person per mode, true door-to-door time including transfers, a normalized comfort metric, and estimated CO2. That data gets checked per country as routes change.

Groups complicate it further: a solo backpacker and a family of four hit different taxi and ferry pricing tiers, so the algorithm has to reweight by group size. Edge cases pile up too: a route with no flight option should just omit that mode, not return an empty ranking; seasonal gaps like ferries not running in winter need flags. None of this is a model call, it's structured logic you build and audit. Get a coefficient wrong and the ranking is wrong for everyone, silently, with no explanation attached.

The payoff

Instant re-ranking, no inference billing, the same result for the same inputs everywhere. And because the logic is deterministic, a wrong ranking is debuggable: trace the variable and weight that caused it. That is worth the maintenance burden. Model calls for a structured problem where users expect reproducible answers are a false economy.

Ready to see it in practice? Build or browse a route comparison here, and watch the ranking shift as you change your travel style.

Top comments (2)

Collapse
 
alexshev profile image
Alex Shev

This maps closely to Google Maps thinking: ranking should be explainable enough that operators can see which signals moved, not just receive a magic order. Language models are useful for summarizing why a route looks good, but the actual scoring needs stable inputs, weights, and tie-breakers.

Collapse
 
road511 profile image
Roman Kotenko

Strong argument, and the honest paragraph about maintenance is the one most posts in this genre leave out. One thing I would push on, because it is where this bites after the system is live: determinism buys you reproducibility, not correctness, and the two feel identical from the inside.

Your sharp edge - "get a coefficient wrong and the ranking is wrong for everyone, silently" - has a twin that is harder to catch: the coefficient is right and an input has quietly stopped describing the world. At ranking time a fourteen-month-old ferry price and this morning's are the same float. The function is behaving perfectly, and a user is being told to take the ferry. With the LLM at least the wrongness was noisy; deterministic wrongness is stable, reproducible and therefore very convincing.

Cheap fix, and it makes the debuggability you are already selling actually reachable: stamp every input with when it was observed, and let each score carry the age of its oldest input. Then tracing a bad ranking ends at "the bus fare for this corridor was last checked in March", not at "the weight is 0.3", and you can degrade or hide a route whose inputs have aged out instead of ranking it confidently.

Your seasonal example is the general case in disguise. "Ferries do not run in winter" is not a flag, it is a validity window on an input, and once inputs have validity windows the two kinds of missing stop being the same thing. Omitting a mode is right when the mode does not exist on that route; it is wrong when it exists and you failed to fetch it, because the user cannot tell those apart and neither can your own logs later. We have the same split as a filter rather than a flag, and it is unforgiving. A third of the planned-construction records we carry - 678 of 2,059 as I write this - publish no dates at all, because the agency simply never fills those fields. A date-window query cannot return them, so it quietly answers a narrower question than the one the caller asked, and the response looks equally complete either way. The fix was not better data, it was saying out loud what the filter reaches: a window query covers 1,381 records, and the other 678 are not outside your window, they are unknown, which is a different answer and deserves to be one.

The group-size reweighting is where I would expect the first real incident, incidentally - tiered taxi and ferry pricing is the input most likely to drift, and it drifts per operator rather than per country, so a per-country review cadence will not see it.