← trainmap.co.uk English · Magyar
Build log
Mapping the whole UK rail network, including the stations that no longer exist
A vanilla-JavaScript map of every railway line and station in the United Kingdom, built entirely on open data. Here's how it came together, the data problems behind it, and the communication that drove it.
I wanted a single map that showed the whole British railway network, not just the bits a journey planner cares about, but the heritage lines, the freight-only curves, the named tunnels and viaducts, and the thousands of stations that used to exist and don't anymore. Nothing quite did that, so I built it: trainmap.co.uk. No app, no sign-up, no account. It loads quickly and you can search it by postcode, station name, three-letter station code, or line name.
There's more to building something like this than the code. The interesting parts are the benefits of being able to visualise the data, the people who use it, and the dozen ways that real-world railway data quietly disagrees with itself. But what actually made the map good was the loop with those people: I shared it early, and let the ones who know the railway best tell me what was wrong. More on that at the end.
An honest review
I've decided to colour code this article so you can see the reality of what decisions were more influenced by me, AI agent assistance or the community.
The colour coding throughout is as follows:
- : the direction, the scope and the judgement calls, technology choices and the code structure.
- What feedback the community gave: bugs, corrections and requests from a railway forum.
- What the AI did: created code for the data pipeline based on my direction, written with an AI coding assistant.
The data problem
Every line and station comes from OpenStreetMap, pulled through the Overpass API. OSM's railway coverage in the UK is excellent, far better than most people assume, because it's been lovingly maintained by people who really care about sidings.
But scoping a query to "the UK" is a trap in itself. A bounding box around Great Britain
drags in Dublin, the Isle of Man, and a worrying amount of northern France.
; the agent's fix was to
ask Overpass for the GB administrative area directly (ISO3166-1=GB)
and clip to it. That gives you Northern Ireland
(it is a UK map) without the Republic, the IoM, or Calais, and with no
hand-tuned latitude fudging that breaks the moment someone tags a new line near a
border.
The lines are fetched from Overpass in tiles: a coarse grid of about thirty cells over
Great Britain, each asking only for railway ways inside the GB boundary, because a
single query for the whole country would simply time out. The fetch is resumable: any
cell already saved and valid is skipped, so a run that dies halfway just fills the gaps.
A build step then dedups the ways by OSM id (the cells overlap at their seams), sorts
each into one of three buckets (core heavy rail, light rail, or disused) by its
railway tag, trims every coordinate to five decimal places (about a metre,
a big size saving for no visible loss), and writes out the GeoJSON the map loads, plus a
small name-to-bounding-box index so you can search for a line by name. All taken care of by a few hundred lines of Python.
The stations that lied
I started with NaPTAN, the UK's official public-transport stops dataset. It's great for the national network, but it has a quirk: it files a lot of heritage-railway stations under tram or metro categories. So when I filtered to "railway stations," whole preserved lines came out half-empty, which is exactly what a forum member meant when they noticed the Swanage Railway was down to a single station.
Stations moved to OSM as well, which then created a different problem: OSM's station tags are inconsistent, so , and the agent worked out the rules:
- Keep National Rail / Underground interchanges: stations whose
networktag mentions "National Rail" (Amersham, Harrow-on-the-Hill). Drop the pure Tube, DLR and tram stops. (That list came almost verbatim from a member who drives those lines and reeled off every Chiltern station I was missing.) - Keep anything sitting on a narrow-gauge heritage line, found by proximity (so New Romney on the Romney, Hythe & Dymchurch survives even though OSM half-tags it as a miniature railway).
Tellingly, the obvious filter, the three-letter ref:crs code, was useless
here, because London Underground stations carry TfL "Z"-prefixed codes in exactly the
same field.
Heritage lines and the Swanage problem
Preserved lines get their own colour on the map. OSM tags them with
railway:preserved or usage=tourism, but again, not
consistently. The Swanage Railway's running line, for example, is tagged as an ordinary
branch on some segments, so it first showed up as a normal National Rail line, until
someone on the forum gently pointed out that I'd "upgraded the branch line to Swanage
from heritage to NR."
The fix is a small heuristic that mirrors how a person reads the map: . If any part of "Swanage Railway" is flagged as preserved, the whole named line goes purple. Crude, but correct far more often than trusting each segment's tags in isolation.
A recurring theme: open data is rarely wrong, but it's frequently inconsistent. Most of the work is writing small, legible rules that reconcile the inconsistency the way a human would.
Lines that snapped straight when you zoomed out
A subtle bug and a good example of the back-and-forth with users. A member reported that the track around Northallerton looked "broken": gentle curves became straight chords at certain zoom levels, and he'd already ruled out his own setup by testing six different browsers. His description (it corrects itself when you zoom into the north end, breaks again when you zoom out, and the south junction won't correct at all) was enough to pin it down.
The cause is that MapLibre simplifies GeoJSON geometry per zoom level by default
(Douglas–Peucker), to keep big vector layers cheap. For most data that's a sensible
default. For a railway map it's exactly backwards: the precise shape of the track
is the entire point. The whole fix was one property on the source:
tolerance: 0. Curves stay true at every zoom. The user confirmed it fixed their
example and a second one he'd spotted near Low Fell. The fix was one line; finding it
was a conversation.
Removing one bug causes another
That tolerance: 0 is a neat example of how one fix quietly sets up the next.
Disabling simplification keeps every vertex at every zoom, which is exactly what you want
for accuracy and exactly what you don't want for memory. The whole network is about 28 MB
of GeoJSON, roughly 720,000 vertices, and it was all being held in memory at once. On a
laptop, fine. On a mid-range phone the browser tab would simply give up, with the dreaded
"Aw, Snap," especially once you switched on another heavy layer.
By moving the geometry-heavy core rail network to , the geometry is pre-cut into tiles per zoom level at build time, and the browser only fetches the few tiles currently on screen, via HTTP range requests. Memory now tracks the viewport, not the country. It's a single static file on the same bucket (no tile server), and the tiling step (tippecanoe) does the per-zoom simplification properly, so the lines stay honest at every zoom and the chord-snapping bug doesn't come back.
The GeoJSON is still there as a fallback, but tiles are the default now. It's the sort of change you don't notice when it works: the map just quietly stops dying on your phone.
The stations that no longer exist
This is my favourite piece. A friend pointed out that Box station, in Wiltshire, was missing. He was right: Box closed in 1965, and it simply isn't in OpenStreetMap at all, neither as a current station nor as a disused one.
So where do you get stations that stopped existing decades ago? Wikidata (CC0). The catch is that its closure data is patchy. Box's Wikidata entry has no closure date at all: the field that should hold one is simply empty, as it is for many closed stations, even though its Wikipedia page will happily tell you it shut on 4 January 1965. (Wikidata does tag it with a vague "decommissioned" status, just without a date.) So filtering on "has a closure date" would miss exactly the stations you're after.
The approach that worked was . Pull every UK railway station Wikidata knows about (about 9,600 of them) and then remove the ones that still exist, defined as sitting within 250 m of a current OpenStreetMap station. What's left, just over 6,100 stations, is the set that used to exist and doesn't anymore. Box included. It's now an optional layer you can switch on, drawn as hollow markers, and you can click any of them through to its Wikipedia article.
(A footnote for anyone who might be interested: the Wikidata queries were run against the QLever mirror, because the official query endpoint is rate limited too heavily.)
The joins are interesting
Step back, and the pipeline is really a handful of datasets stitched together, and the catch is that none of them share an identifier. OpenStreetMap supplies the geometry and the live stations, but tags them inconsistently and knows nothing about stations that have closed. The closed ones come from Wikidata, often (as we saw) without even a closure date. Postcode lookups, at search time, come from postcodes.io (ONS data underneath). NaPTAN, the official stops register, was where the stations started, until it turned out to file too many heritage ones under tram and metro, which is exactly why they moved to OSM. Each source is authoritative for something and useless for the rest.
With no shared key to join on, almost every meaningful decision in the script is made by geography or by name instead:
- Is an OSM "metro" station really a National Rail interchange? Read its
networktag: that's Amersham and Harrow-on-the-Hill back in. - Is a station on a heritage narrow-gauge line despite being tagged a miniature
railway? No tag says so, so test whether it sits within a hundred metres or so of a
narrow_gaugeline in the core data (that's how New Romney survives). - Is a whole line heritage? Propagate
railway:preserved/usage=tourismacross every segment that shares a line name (the Swanage fix). - Is a Wikidata station still open, or long gone? Subtract: within 250 m of a current OSM station it counts as current; otherwise it's one of the ~6,100 that closed.
A couple of those are frankly inferences rather than facts: the heritage colour and the "former station" label are derived, not stated by any source. Nothing is invented (every position and name still traces back to OpenStreetMap, Wikidata or ONS), but they're heuristics, and a heuristic is only as good as the cases you test it against.
Which is the real reason the forum mattered. I could write the rules; I had no reliable way of knowing whether they were right: whether the Swanage propagation caught every segment, whether the narrow-gauge test was too tight, whether a "covered" tag at Ardwick should really count as a tunnel. The people on that thread did. Every correction was, in effect, a failing test case for the pipeline, written by someone who knew the territory far better than I could. I only got the reconciliation right because and they told me what was wrong, or could be improved.
Finishing touches
- ,
a station name, a three-letter code (
PAD,YRK), , in which case the map zooms to fit the whole route. - Named structures. Principal tunnels and viaducts are labelled from
OSM's
tunnel:name/bridge:nametags: Severn Tunnel, Ribblehead Viaduct, and so on. (That one was a direct request: "the icing on the cake would be to name the principal tunnels and viaducts.") - Click a station (present or long-closed) for an info button to its
Wikipedia article and a button to its location on Google Maps. About 92% of current
stations and 99% of former ones resolve to a Wikipedia page, mostly straight from
OSM's own
wikipediatags.
What actually made it good
, not looking for anything, just to see if anyone liked it, then spent the next couple of days shipping fixes while the thread was still live. There was no plan and no roadmap; the roadmap was whatever the last person had spotted, and most of it went live within hours of being raised.
None of that is novel; it's the oldest idea in software, the one the Agile Manifesto wrote down before "agile" came to mean a calendar full of meetings: . Process makes that easy to lose. A public thread full of people who genuinely cared handed it straight back to me, and it's the part of building this I'd most recommend copying, whatever you happen to be making.
trainmap.co.uk is free and ad-free.
If it's useful, please consider