DEV Community

Cover image for Mapping Roman Britain: an AI-built viewer for Digital Britannia
Adrian Cockcroft
Adrian Cockcroft

Posted on

Mapping Roman Britain: an AI-built viewer for Digital Britannia

The Roman Roads Research Association (RRRA) has released Digital Britannia v1,
a GeoPackage of every engineered Roman road, fort, camp, settlement and
milestone in Britain. I wanted a web map for exploring it properly: against
LiDAR, against historic Ordnance Survey maps, and against other datasets.
So I handed Claude Code a brief and watched it build one.

It's an unofficial viewer. The data is © Roman Roads Research Association /
Mike Haken, CC BY-NC 4.0.

Overview of the viewer: roads, forts, settlements and camps over LiDAR

The brief

I wrote the brief in a Claude session discussion about what I wanted on my phone, then on my desktop created a new GitHub repo, and pasted the brief in to Claude Code:

The full brief, as pasted

Pass 1: build a web map viewer for the RRRA's Digital Britannia v1
GeoPackage (about 3 MB). Non-commercial; credit the RRRA and Mike Haken; mark it
unofficial.

  • Layers: engineered roads (3,572 lines, with a Segment Confidence from 3 = certain ±30 m down to 0 = no evidence), military installations, temporary camps, settlements, milestones, milecastles, and walls.
  • Normalise Main site type, which has inconsistent capitalisation, and one garbled settlement TYPE. Follow the author's QGIS styles in the layer_styles table.
  • Plan: Python with pyproj and shapely to make GeoJSON; a MapLibre GL viewer with roads styled by confidence and labelled with Margary numbers, a "possible" toggle, a period slider, popups, search with route highlighting, and basemap plus overlays; one self-contained page if the data stays under ~5 MB, otherwise PMTiles.

Pass 2:

  • No simplification. Keep every vertex; round coordinates to 7 dp.
  • PMTiles only if over 5 MB (tippecanoe --no-line-simplification, zoom 18+).
  • LiDAR hillshade is core, not optional: EA for England, DataMapWales for Wales, the Scottish Remote Sensing Portal for Scotland. Verify each URL and licence first, prefer multi-directional hillshade, and draw roads with a halo plus a toggle to hide them.

Pass 3: a list of candidate underlays and overlays: NLS historic maps,
aerial imagery, Historic England aerial mapping, scheduled monuments,
Canmore/Coflein, BGS geology, 3D terrain, DARE, AWMC, Pleiades, OS maps.
"I'd make NLS 6-inch, aerial imagery and LiDAR the core three, with the rest
optional." Also: these come from general knowledge; verify each endpoint and
licence before wiring it in.

Mid-build, I also asked for architecture decision records (ADRs) for every
decision, and an errata document I could send back to the RRRA.

That last instruction, verify before wiring it in, shaped the whole build.
More on that below.

Timeline

Time Commit What landed
14:12 — Repo cloned
14:43 6a111df Data pipeline, viewer, 11 ADRs, errata, browser-tested
14:52 f77ba37 Scottish LiDAR; documented why Wales isn't possible
15:04 fff4fc6 Optional extras: DARE, geology, 3D, heritage overlays
15:10 3ae0722 Itiner-e comparison roads
15:25 b141c8e Datum fix for Itiner-e (see below)
15:28 ca52875 Data no longer waits for slow tiles

Step 1: full-fidelity data, and a single page after all

The pipeline (scripts/process.py) reprojects each layer from British National
Grid to WGS84. It keeps every vertex and only rounds coordinates to 7 dp
(about 1 cm). The road accuracies are stated as ±30 m and ±200 m, and
simplifying would quietly degrade them.

The result is 4.19 MiB of GeoJSON, under the 5 MB line. So there's no
PMTiles and no tippecanoe, just one index.html on GitHub Pages fetching seven
files.

Cleaning the categories turned up more than the brief anticipated:

  • The garbled value: Military vicus - size uncertaMilitary vicus - size uncertainin
  • Capitalisation: Legionary Fortress (1 record) vs Legionary fortress (11)
  • Glued NULLs: the literal text NULL stuck to the front of values, which turned one Heritage Gateway link into NULLhttps://…
  • Whitespace: 592 values padded with spaces
  • Road numbers: 239 distinct non-standard forms (220?, 1b & 1c, 85-link vs 85 link)

Every issue went into an errata document with counts and examples, written
for the RRRA's next release. The viewer applies non-destructive fixes and
records each one.

Step 2: the viewer

MapLibre GL JS, with no build step and no API keys. Roads follow the author's
own QGIS style: solid for certain, dashed for probable, dotted for conjectured,
each with a white halo so it stays readable over the underlays. Click a road and
its whole Margary route lights up.

Watling Street (RR1f) over Environment Agency LiDAR, with the whole route highlighted

Search takes place names, Roman names and road numbers:

Searching "RR2" lists the Ermine Street family
Selecting RR2a highlights the route and frames it clear of the panel

There's also a period slider for military sites (131 Claudio-Neronian sites,
peaking at 288 Flavian), popups with every attribute, and a bottom-sheet layout
on phones.

Phone layout

"Verify before wiring it in" paid off, repeatedly

Several services looked right from a quick check and weren't. Each was only
caught by looking at the actual output.

CARTO base maps returned HTTP 200 and were useless. A curl check passed.
In the browser, every tile said this:

CARTO's keyless tile

The 2 KB watermark is what CARTO now serves to keyless requests. The base maps
switched to Esri's Light and Dark Gray Canvas, and the lesson went into an ADR:
a status code doesn't verify a tile service; you have to look at the tile.

The NLS "6-inch first edition" covers Scotland only. Every England tile
returned 404. Listing the S3 bucket and reading each tileset's metadata.json
showed that 6inchfirst stops at 54.6°N. The seamless GB layer is
6inchsecond. Here it is at Housesteads, where the RRRA Wall, Vallum and
Military Way line up with "Borcovicus Roman Station" on the OS sheet:

NLS OS 6-inch 2nd edition at Housesteads, with the Digital Britannia layers on top

Label fonts: one public glyph endpoint returned an HTML page; two others
returned 404. One worked.

LiDAR across three countries: three different answers

England was straightforward. The Environment Agency's 1 m composite
hillshade WMS supports Web Mercator, so it drops straight into MapLibre.

Scotland publishes LiDAR DTMs but no hillshade. Its layers only come
with colour-ramp elevation styles, which hide subtle earthworks:

Scottish DTM with the published colour-ramp style

The portal runs GeoServer, though, which accepts an inline style. Passing a
ShadedRelief symbolizer via SLD_BODY makes the server compute the
hillshade per request
, here at the Antonine Wall:

The same DTM shaded on the server

One request stacks 11 survey layers (phases 1–6, the National LiDAR Programme
and four Historic Environment Scotland surveys), with newer data drawn on top.
That's about 4.9 KB of URL, safely under common GET limits. Here it is at
Inchtuthil legionary fortress:

Server-side hillshade at Inchtuthil

Wales: not possible yet. The Welsh hillshades exist, including a
multi-directional one, but they aren't in DataMapWales's public WMS. The public
cloud-optimised GeoTIFFs (1–22 GB) have no CORS rule; the preflight returns
403 CORS not enabled. No web page can read them. The ADR records exactly what
would unblock it.

Optional extras, and what didn't make it

Added after the same checks:

DARE Roman Empire map BGS geology at Bath
DARE Roman Empire map: Latin names line up with the RRRA forts BGS geology at Bath (Aquae Sulis)
3D terrain on Hadrian's Wall HE aerial mapping and a scheduled monument
3D terrain along Hadrian's Wall Historic England's Roman-period aerial mapping (a cropmark road ditch) and scheduled monuments

Also added: scheduled monuments for Scotland (HES) and Wales (Cadw), and
Pleiades links for sites with a Roman name.

Rejected, with reasons recorded:

  • AWMC/CAWM ancient-world tiles: http only, so blocked on an https page.
  • Canmore: its WMS gives no titles, abstract or licence.
  • Coflein: no service.
  • OS Maps API and Bing: need keys.

The datum bug in someone else's data

The last addition was Itiner-e, a CC BY 4.0 dataset of roads across the
whole Roman Empire, as a comparison layer. Clipped to Britain (after a box
caught eight roads on the Normandy coast), it's 1,371 segments.

Then Adrian noticed something while looking around Dorchester: the Itiner-e roads look consistently offset.

Before: Itiner-e (magenta) runs parallel to the RRRA roads, about 90 m off

A route disagreement wanders. A consistent offset points to a coordinate
system problem. Claude fitted the single best shift between the two datasets,
region by region, and compared it with the error you'd get by treating
OSGB36 latitude/longitude as if it were WGS84:

Area Best-fit correction (E, N) Predicted OSGB36-as-WGS84 error
Dorchester −90, +69 m +92, −64 m
Rochester −109, +59 +118, −55
High Cross −102, +38 +103, −41
York −111, +12 +105, −22
Hadrian's Wall −94, +9 +96, −8

It's a mirror image, right down to the north–south part shrinking towards the
Wall, the way the datum difference does. The offset showed up in all three
contributor groups.

Before blaming Itiner-e, Claude checked its own pipeline. The same signature
would appear if our British National Grid conversion had skipped the datum
shift. PROJ uses a 2 m-accuracy OSGB36→WGS84 transformation, and the Greenwich
Airy transit circle converts to 0.0016° W, not 0.0000°. So the RRRA layer was
right.

The fix is principled, not a fitted nudge: reinterpret Itiner-e's British
coordinates as OSGB36 (EPSG:4277) and convert them properly.

After: the two datasets coincide; the one remaining divergence is a genuine difference of route

The median gap fell from 81 m to 17 m, and the remaining best-fit shift is
+1, −2 m. That beats the best constant shift (24 m) because the datum
correction varies across the country the way the error does. It's written up in
ADR 0013, ready to send to the Itiner-e team.

Bugs found by actually using it

Claude tested everything in a real browser, and that's where most of the bugs
surfaced:

  • Inscribed spellings: Roman names are stored as inscribed (VERCOVICIVM), so searching "Vercovicium" found nothing. A small V→U normaliser now feeds both search and the Pleiades links.
  • Silent layer failure: a MapLibre zoom expression was nested inside a match. MapLibre reports that as a warning event, not an exception, so the layer silently didn't exist while the panel said it had loaded.
  • Data held back by slow tiles: a link to a Scottish view showed no roads for 30+ seconds. The data fetch sat inside MapLibre's load event, which waits for every visible tile, and the Scottish hillshade is rendered per request. The fix was to start fetching immediately and add layers on style.load. Now the roads appear in about 4 s while the hillshade fills in:

Roads and the Inchtuthil fortress appear before the slow hillshade tiles finish

  • Claims corrected before they shipped: some of the agent's own statements were checked and fixed. An errata line said "confirmed" before anything had been counted (it was then counted: 7 of 517). An ADR named a licence the service never states.
  • A wrong diagnosis, caught the next day: the LiDAR hillshade sometimes had blank patches. While capturing screenshots, Claude found tile requests with status 0. It put them down to requests cancelled by view changes, plus brightness patchwork between EA surveys, and withdrew a retry fix it had floated. That was wrong. The next day, working on an unrelated issue, a fresh load with no view changes still lost 11 of 30 tiles. Measuring properly showed the real cause:
    • The EA server fails some tiles transiently from browsers: 7 of 29 stayed broken on the live site, and bursts of 30 lost 0–8.
    • Every failed tile succeeds when requested again.
    • MapLibre never retries a failed raster tile, so each failure stayed a hole.

The fix routes the LiDAR sources through a small retry:// protocol with
back-off. It was verified by forcing the first attempt of every third tile
to fail: 14 of 42 were forced, and all 42 loaded. The lesson: a plausible
explanation isn't evidence. The status-0 requests should have been tested,
not explained away.

Day 2: fixing what I found by using it

After a day of using the live map, Adrian filed two GitHub issues and gave Claude
Code a three-word prompt: check the issues. It read them, did the one it
could start straight away, and asked before the one that needed a download.

Time Commit What landed
08:55 — Issue #2 opened; work starts
09:00 e350dd2 3D camera sliders (closes #2)
10:07 5924189 Pleiades places matched at build time (closes #1)
10:17 02d30bb LiDAR tile retries (the wrong diagnosis above)

Issue #2: the 3D view needed controls

Tilting with the mouse was the only control, and the right-drag gesture didn't
work for me. Ticking 3D terrain now shows four sliders: Tilt,
Rotation, Altitude and Relief (vertical exaggeration). They follow
the camera when you move the map, and switching 3D off returns a flat,
north-up view. MapLibre caps tilt at 60° by default; that's now raised to 85°.

3D sliders tilted along Hadrian's Wall

Issue #1: the Pleiades links didn't work

The popups linked to a Pleiades search for each site's Roman name, and they
often missed. Dorchester's is a clear case: the dataset spells names as inscribed (DVROVARIA),
Pleiades uses the standard form (Durnovaria), and its search only matches
exact words. My issue sketched the fix: download the Pleiades open data, find
candidate places within 2 km of each site, fuzzy-match the names, and link
straight to the best match.

The first run matched 515 sites, but the review step is where it got
interesting:

  • The biggest towns missed. Pleiades has "Col. Glevum" and the dataset has GLEVVM. Pleiades has plain "Venta", while the dataset adds the tribe: VENTA BELGARVM. Itinerary forms differ in case ending too (BOMIVM vs Bomio). Scoring the best distinctive word, with Latin endings stemmed, fixed Gloucester, Colchester, Lincoln, Winchester, Wroxeter, Canterbury and Chichester.
  • Some links went to the wrong part of a site. The fort at Chester linked to "Roman amphitheater at Deva", and Caerwent to its town walls: the monuments matched the name just as well, and sat nearer. Ties now go to a town or fort (from Pleiades' featureTypes) over a monument inside it.
  • Modern-name matching needed guard rails. Generic words ("Roman", "fort", "hill") would have paired any two forts within 2 km, so they're stripped before comparing. Popups label these links "matched by modern place name".

The result: 533 sites link straight to their Pleiades place (219 by Roman
name, 314 by modern name). The other 466 fall back to a search and are listed
in a CSV for manual review; only 17 of them have a real Roman name. The
matches are committed, so a fresh clone rebuilds identically without the
download. Roman names now display in title case, and searching "Durnovaria"
finds Dorchester:

Searching "Durnovaria" opens Dorchester with a direct Pleiades link

What turned up along the way

  • 67 road segments (1.9%) have no line at all. They have full attributes but a NULL geometry in the GeoPackage, including stretches of the Fosse Way, Dere Street, Sarn Helen and the Military Way. They had never been on the map, and they broke "Zoom to whole route": choosing RR13 in search threw an error.
  • More errata for the RRRA: the same place spelled differently in two layers (PONS AELIVS / PONS AELIUS), placeholder text where a name should be empty (29 "unknown", and an "unkonwn"), 3 blank settlement rows, and 2 duplicated camps.
  • The LiDAR holes, described in the previous section: the bug I'd explained away on day one, found and fixed properly.
  • A way back to the repo. The top of the panel now has Star repo, Issues and New issue buttons that open GitHub in a new tab, so anyone using the map can report a problem the same way I did.

The documentation is a deliverable too

  • 14 ADRs. Each records the context, the decision and the evidence (curl output, tile checks, measurements), including the decisions that changed during the build.
  • ERRATA.md for the RRRA: 27 issues, each with a severity, a count and a suggested fix.

Build time and token usage

Day 1: the build Day 2: the issues
Wall-clock ~76 min (14:12–15:28), including my pauses to revise the brief ~81 min (08:55–10:17), including waiting for my OK on a download
Commits 7 3
Share of usage limits ~4% of the 5-hour and weekly limits ~4% of the 5-hour, ~2% of the weekly (a new week)
Context window at the end ~503k of 1M tokens (50%) ~676k (68%), the same conversation
Extra (pay-as-you-go) spend $0 $0

Models: Claude Opus 4.8 for the initial build, then Opus 5.5 from before the
first commit.

The context figure is what the conversation held at the end. It isn't a
cumulative total: each turn re-reads the context, mostly from cache, so the
tokens actually processed are several times higher. The tooling didn't expose
that total.

What's next

  • Wales LiDAR, if DataMapWales publishes the hillshade in its WMS or enables CORS on its COGs.
  • Report the datum issue to the Itiner-e team, and send the errata to the RRRA.
  • Review the 466 unmatched Pleiades sites. Some need only a wider radius: Petuaria, at Brough, has no Pleiades place within 2 km.
  • Pin the remaining endpoints, or self-host what can be self-hosted. The label glyphs come from MapLibre's demo server, which has no availability guarantee.

Credits

  • Digital Britannia v1: © Roman Roads Research Association / Mike Haken, CC BY-NC 4.0
  • LiDAR, England: contains Environment Agency information © Environment Agency and/or database right
  • LiDAR, Scotland: © Crown copyright Scottish Government, SEPA, Fugro, Historic Environment Scotland and partners, via the Scottish Remote Sensing Portal (OGL)
  • Historic maps: National Library of Scotland
  • Roman Empire map: DARE, Johan Åhlfeldt (CC BY 4.0)
  • Places: Pleiades, © the contributors (CC BY 3.0)
  • Comparison roads: Itiner-e 2024, Brughmans, de Soto, Pažout & Bjerregaard Vahlstrup (CC BY 4.0)
  • Heritage, England: © Historic England (OGL)
  • Heritage, Scotland: © Historic Environment Scotland (OGL)
  • Heritage, Wales: Cadw (OGL)
  • Geology: contains British Geological Survey materials © UKRI
  • Imagery and base maps: © Esri and partners
  • Terrain: Mapzen terrain tiles on AWS Open Data

Top comments (0)