← 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.

By optional.org.uk · 31 May 2026 · trainmap.co.uk ☕ Buy me a coffee

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 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. I noticed those stray foreign lines; 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 I had to decide what counts as a heavy-rail or heritage station, and the agent worked out the rules:

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: propagate the heritage flag across every segment that shares a line name. 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.

The honest fix isn't to throw detail away; it's to stop loading data you can't see. By moving the geometry-heavy core rail network to vector-tile-based rendering (PMTiles), 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 a subtraction. 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:

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 I put it in front of enthusiasts and they told me what was wrong, or could be improved.

Finishing touches

What actually made it good

I shared the map on a UK railway-enthusiast forum early and rough, 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: a short, honest loop between the people using a thing and the person able to change it, and a bias to act on what they say now rather than later. 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.


Open the map →

trainmap.co.uk is free and ad-free.
If it's useful, please consider

☕ Buy me a coffee