← Blog

How CityWalker divides cities into regions

Add Vienna and you get 23 named districts. Add Sacramento and you get 15 neighbourhood groups. Add a city that is not in the app yet and you get nothing at all. Here is what determines that, and how the boundaries are drawn.

Why the app uses regions at all

A large city has tens of thousands of street segments. Without structure, the coverage map is just a number that climbs slowly. Regions give you a unit of completion that sits between a single street and the whole city: you can finish Centrum, see that you have barely started Noord, and know exactly where to go next.

The challenge is drawing those regions consistently across every city the app supports, without the shapes feeling arbitrary or the names feeling wrong.

The boundaries come from OpenStreetMap

For most cities, the regions map directly onto existing administrative boundaries in OpenStreetMap: the districts, boroughs, or neighbourhoods that local authorities have mapped and named. Vienna's 23 Bezirke, Tokyo's wards, Barcelona's ten districts, all already in OSM and drawn by contributors who live there.

Finding them is not as simple as it sounds. OpenStreetMap uses a numeric admin_level tag to classify boundaries, but the scale is not standardised across countries. In Germany, city districts sit at level 9. In Japan, Tokyo's wards are at level 7. A query that works for Berlin finds nothing useful for a city in India, because the same tag means something different in each country's mapping conventions.

The generator handles this by probing levels starting just below the city's own level and working upward until it finds at least two child boundaries. For most cities this lands on the right level automatically. For a few, it needs a manual override.

When there are too many districts

Some cities are mapped at fine granularity. Wrocław has 48 osiedla at the district level. That is too many to be useful in the app: a list of 48 regions stops being navigable. When the district count is above twenty, the generator clusters them into a smaller number of groups using k-means on each district's centre point.

Each cluster polygon is the union of its members. The cluster takes the name of whichever district is closest to the group's centre, so the labels stay geographically grounded rather than becoming generic zone numbers. Wrocław's 48 osiedla become 9 named regions that cover the same ground.

When OSM is not the right source

For some cities, OpenStreetMap either has the wrong level of detail or nothing useful at all. In those cases the generator can use the city's own published data instead.

Sacramento is an example. OpenStreetMap has neighbourhood boundaries for the city, but at a level of detail that produces 129 separate areas. The city's open data portal publishes a GeoJSON of 15 curated neighbourhood groups that better match how residents actually think about Sacramento. Switching to that source produced far cleaner regions.

Ingolstadt is a different case. OSM has no mapped district boundaries below the city level at all. The city publishes its Stadtbezirke as a Shapefile on their open data portal. The generator reads that directly and produces the same output as the OSM path.

The variation you see between cities (why some have neat district names and others have slightly broader zones) reflects which source had the best data for that city.

How the boundaries reach the app

The original design queried OpenStreetMap live when you added a city, but the service was unreliable for exactly the large cities where regions matter most. The regions are now generated offline, reviewed against an interactive preview map, and committed to a public repository. The app checks for an updated set once a day and caches it locally, so region boundaries are available the moment you add a city, with no dependency on any external service being available.

Each region is stored as an encoded polyline alongside a bounding box. Encoded polylines are significantly smaller than equivalent GeoJSON, which matters because the full set of boundaries ships bundled inside the APK. The app uses the polyline for precise point-in-polygon containment and the bounding box for fast pre-filtering.

Adding your city

If your city is not in the app yet, the boundaries repository is the place to add it. The generator script handles the OpenStreetMap queries, the clustering, and the encoding. It also writes an interactive HTML preview so you can check the regions before submitting. PRs are welcome; the contribution guide walks through the full process.


Questions about why your city is divided the way it is? Email me.