Expanding the routing graph to Great Britain
Status: proposal, not scheduled. Written 2026-08-05.
Journey planning is the last part of the stack still limited to one region.
Stops, places, bus departures and rail departures all cover Great Britain;
/v1/journey.json covers South and West Yorkshire, because point-to-point
planning needs a street network to link the origin and destination to the
transit graph, and the graph was built from two Geofabrik county extracts.
Where we are
| Current | GB-wide | |
|---|---|---|
| OSM input | 80 MB (S+W Yorkshire) | ~1.9 GB (great-britain-latest.osm.pbf) |
| GTFS input | 121 MB zipped | 1.2 GB zipped / 7.1 GB unpacked |
| Serialized graph | 239 MB | ~4–8 GB (extrapolated) |
| OTP heap | -Xmx4G, ~1.9 GB resident |
see below |
The national bus GTFS is already on this host — /opt/transcapi/data/uk_bus_gtfs.zip,
loaded into TransCAPI's own database. No new data acquisition is needed for bus;
the rail feed is already national too.
The blocker is this machine
RAM: 7 GB total, ~1 GB available
CPU: 4 vCPU
Disk: 45 GB free
OTP holds the whole graph in heap. A GB build realistically needs 32 GB heap minimum (48–64 GB comfortable), and 16–32 GB to serve afterwards. This host cannot do either. Nothing in the build process can be tuned around a 25× shortfall.
So the expansion is not a code change — it's a capacity decision. The work below only becomes schedulable once there's a machine to do it on.
Options
1. Scale this host up (simplest)
Resize to 64 GB for the build, run it, then settle at 32 GB to serve. One graph, national coverage, no routing logic changes, no cross-region seams. Costs the difference in instance size permanently.
2. Build elsewhere, serve here
Build graph.obj on a large short-lived instance and ship it over. Saves the
build-size instance cost, but serving still needs 16–32 GB, so this host still
has to grow — it only avoids the peak. Worth it if builds are infrequent.
3. Regional graphs behind multiple routers
Shard into ~6 regional graphs, run them as separate OTP routers, and pick one by bounding box at request time. Each fits comfortably in current-generation memory. The cost is real: journeys crossing a shard boundary can't be planned, which is exactly the Leeds→Manchester kind of trip a national API is expected to answer. Recommend against unless capacity is immovable.
4. Trim the inputs (do this regardless)
Independently of which option above, the feed can be cut substantially before it ever reaches OTP:
- Drop
shapes.txt— 2.2 GB of the 7.1 GB unpacked feed, 31% of the total. OTP falls back to straight lines between stops for transit leg geometry. Street legs (walking, cycling) keep true geometry from OSM either way. If the frontend can live with less precise bus-route polylines, this is the single biggest win available. - Filter the service window.
build-config.jsonalready setstransitServiceStart: -P1M/transitServiceEnd: P6M; trimmingcalendar_dates.txtandtrips.txtto that window before the build avoids loading services OTP will discard anyway. - Drop
frequencies.txtif no GB operator uses frequency-based scheduling (worth checking — it's only 2.4 KB, so this is tidiness rather than savings).
Trimming might bring a GB graph within reach of ~16 GB rather than 32 GB. That changes the instance-size conversation materially and should be measured before committing to a machine size.
Suggested sequence
- Trim a GB feed per option 4 and measure the actual unpacked size.
- Do one throwaway build on a rented 64 GB instance to get real numbers — peak build heap, serialized graph size, steady-state serving heap. Everything above is extrapolation from a 240 MB graph and should not be used to size a purchase.
- Size the target host from those measurements, not from this document.
- Build, ship
graph.obj, updateJOURNEY_COVERAGE_BBOXandJOURNEY_COVERAGE_NAMEin TransCAPI's.env, and drop the coverage warning from the journey docs page.
Step 4 is the only part touching TransCAPI. The coverage guard added on 2026-08-05 reads its bounds from config precisely so that widening coverage is an environment change and a docs edit, not a code change.
Interim behaviour
Until then, an out-of-area request returns 422 outside_coverage naming the
covered area, rather than an empty itinerary list. Callers can tell "we don't
plan here yet" apart from "no journeys found", which is the distinction that
matters when deciding whether to retry.