Why a train on the map is never exactly where the train is
There is no GPS on a British passenger train that you can read. The dots on the live map are driven by the signalling system, and once you know how that works, everything odd about them stops being odd.
A train on our live map is not being tracked. It is being described — and the difference explains almost every question people ask about it.
Berths, not coordinates
Britain's railway is divided into train describer areas: 194 of them in the data we hold. Within an area, the track is divided into berths, each roughly the section of line between one signal and the next. A signaller's screen shows a train's four-character headcode sitting in a berth. As the train passes a signal, the equipment steps the headcode from one berth into the next and publishes a message saying so.
That message is the entire basis of the map. It says, in effect, “headcode 1S23 has moved from berth A to berth B in area M1”. It contains no latitude, no longitude, no speed and no heading. Turning it into a dot means looking berth B up in a reference dataset called SMART, which gives the location code beside that berth, and then looking that code up again to get coordinates.
So the first thing to understand is that a berth is an area, not a point. The dot marks the section of line the train is somewhere within. On a dense approach to a terminus a berth might be a couple of hundred metres; out on a rural single line it can be several miles. The dot is accurate to a signal section, and no better.
Why half of all movements don't move the dot
Here is a number that surprised us when we measured it. The SMART dataset we load describes 34,194 berth steps covering 22,359 distinct berths — but those berths resolve to only 3,766 distinct locations. That is an average of very nearly six berths sharing one location code.
The consequence is unavoidable: when a train steps between two berths that share a location, the message arrives, the train genuinely moved, and the dot does not budge. Roughly half of all steps are like this. A train can report four movements in a row while appearing frozen on screen, because all four berths hang off the same location code.
This is also why the dots advance in jumps rather than gliding. A real position update is a discrete event at a signal, not a continuous stream. We smooth this by moving each dot forward between reports at the pace its timetable implies — anchored to the last real observation, so a train running twenty minutes late stays twenty minutes late rather than being quietly dragged back onto schedule.
Whole lines with no dots at all
SMART maps only about half of what the describers actually report. This is not a gap we can close: some lines simply have no berths recorded in it. The Ribble Valley line is the example we keep coming back to — Ramsgreave, Langho, Whalley and Clitheroe have no berth mappings whatsoever, so a train working that route used to freeze at the last mapped berth before it and sit there until it reappeared somewhere else entirely.
That looked exactly like a bug, and it took a while to accept that it was the data. The fix was not to try harder with SMART but to add a second kind of dot: for trains the describers cannot place, we position from the booked timetable instead, interpolating between calling points. Those dots are honest about being estimates. They are also the reason the map does not have holes in the shape of entire branch lines.
The same headcode, in two places, legitimately
A headcode is not an identity. It identifies a train to a signaller, within an area,
and it is reused freely across the country. In a single sweep of the live map we found fifteen pairs of genuinely different trains sharing a headcode at the same
moment — not duplicates, not errors, just different services that happen to be 1A20 in different parts of Britain.
Early on we keyed the map's animation on the headcode alone, and the result was a dot that slid hundreds of miles across the country between refreshes as it was matched to the wrong train. Identity on the map is now the describer area and the headcode together.
There is a related trap worth knowing if you ever read raw feeds: 0B00 is not a
train. It is a placeholder used network-wide for stock movements with no allocated headcode,
and treating it as an identifier merges hundreds of unrelated movements into one imaginary
service.
What the colours are actually telling you
Each dot carries two independent signals. The fill is the operator's brand colour, so you can pick out one company's services at a glance. The ring is lateness.
Unknown lateness has its own colour and is deliberately never green. About an eighth of the trains on the map at any time are freight, which the passenger forecast feed does not cover at all, so we have no delay figure for them. Colouring those green would be stating that they are on time, which we do not know. A separate colour says “no information”, which is the truth.
A dot also fades after three minutes without a report, and is dropped entirely after fifteen. A stationary train and a train we have lost track of should never look the same.
What this means when you use the map
- A dot that has not moved is not necessarily a stopped train. It may have stepped between berths sharing a location, or the reporting may have gone quiet — check whether it has faded.
- Position is accurate to a signal section. Do not read a dot's exact placement as the front of the train.
- A missing train is usually an unmapped berth, not a cancelled service. The departure board is authoritative on whether a train is running; the map is authoritative on roughly where it is.
- Freight is on the map but has no lateness data. The ring colour tells you which trains we can speak to.
Where this comes from
The berth steps are Network Rail's TD feed; the berth-to-location mapping is SMART; the location-to-coordinates step uses CORPUS and our own station and junction database. All are listed with their licences on where the data comes from, and how it works covers the rest of the site's mechanics. If you want the same data programmatically, the API serves it.