Every time the map comes on screen, the app has to answer one question: how much of this area have you actually explored? It sounds like a rendering question. Most of the work is not.

The territory layer has been the most interesting engineering problem in Roam City so far, because every simple answer to that question turns out to be wrong in a visible way.

The obvious answer, and why we dropped it

The first version counted tiles: how many small squares on the map had been visited, out of how many exist. Easy to compute, easy to explain, and misleading. Tiles are an artifact of rendering. Change the zoom level or the tiling scheme and the number moves, even though the person did not.

We index space on an H3 hex grid, which is excellent for lookup and aggregation. We still refuse to show an H3 cell ratio to a person. It is an index, not the thing being measured.

Count streets, not squares

Territory progress is measured in walkable length: how many metres of walkable road or path you have touched, out of the total walkable network for the area. The denominator comes from a curated street network, filtered down to what a person can actually walk.

  • Motorways, rail, ferries and private-access ways are left out of the denominator.
  • The same network produces both the numerator and the denominator, so the two cannot drift apart.
  • When the network is re-imported or corrected, everyone’s progress is recomputed against the same rules.

Twenty metres at a time

A long road segment is not one unit. It is cut into short chunks of roughly twenty metres. Passing the start of a street should not hand you the whole street, so progress is the sum of the chunks you actually touched.

The touches come from the discovery tiles the app already syncs during a session, not from raw GPS uploads. A tile counts as touching a chunk when it lands within about twelve metres of it, roughly half a chunk plus the tile itself. The rule is forgiving of GPS noise and stingy with credit.

A hex cell is a bucket, not a street. Progress should describe a place, not a rendering.

The same answer, twice

If the same walk can be processed more than once, it must not add progress twice. Work is enqueued under a coarse H3 bucket so duplicate submissions collapse, and the data model makes double counting impossible at the database level: progress is unique per person and area, contacts unique per person and chunk.

The heavy matching runs in PostGIS with spatial indexes, so a tile is tested against nearby chunks instead of every chunk ever imported. That is what keeps a dense city centre from turning into a slow query.

Drawing a city without drawing everything

Rendering has its own trap. At a city-wide zoom, drawing every H3 cell and every chunk would melt the frame rate and tell the person nothing. Geometry is scoped to the viewport and the zoom level; coarse zooms show a summary instead of the raw cells.

Territory fill and outline sit below roads and labels. The missing walkable network only appears for the selected area, and only when you are close enough for individual streets to mean something. The map should get simpler as you zoom out, not busier.

Calm by design

Territory is a personal progress layer, not ownership. There is no land grab, no competing with someone else’s map, and no mechanic that takes away what another person has explored.

Status thresholds live in versioned server rules rather than values scattered through the interface, and milestones are idempotent: a milestone is acknowledged once, not on every map load. From the selected area you can ask for the parts you have not walked, and the route engine that already exists turns that into a real walking route.

None of this shows up in a screenshot. It is the reason the number on screen stays honest as the map grows, which is the only part that matters.