Turning OpenStreetMap into a dataset a phone can use surfaced several defects. None of them threw an error. Every one produced output that looked entirely reasonable, which is why they are worth writing down — anyone doing this work is likely to hit the same ones and have no reason to look.
Written up in the order they would bite you.
Speed limits expressed in mph were silently dropped
OpenStreetMap's maxspeed tag is a string, and most of the world writes a bare number meaning km/h: maxspeed=50. The United Kingdom, the United States and a few others write maxspeed=30 mph.
Our first parser accepted bare integers and ignored anything else. It did not crash or warn — unparseable values simply became "no limit known", which is a legitimate state that appears constantly in real data.
The result: a test over Great Britain returned 215 usable speed limits where the correct answer was 858,707. The output was not empty, so nothing looked broken; the country merely appeared to be one of the world's worst-mapped rather than one of its best.
If you are doing this: parse the unit. maxspeed also admits walk, none, signals, variable, and country-code implicit values like DE:urban. Decide what each means for you, and count what you discard — a parser that silently drops values is indistinguishable from a country that has none.
Region file names collided, and the United States vanished
We derived an internal key for each region from the basename of its download path. Geofabrik publishes US states under north-america/us/california, so california became the key.
Two things broke at once. Every one of the 52 US regions produced a key that no longer matched the path it came from, so none of them were linked to a country — the United States was absent from the published catalogue entirely, and the count of 188 countries looked plausible enough that nobody questioned it.
Worse, europe/georgia and north-america/us/georgia both reduced to georgia. One overwrote the other. The file on disk was 96 MB, which is the size of the country; the US state had never been downloaded at all. A rider in Atlanta would have been handed the road network of Tbilisi, and the app would have reported no roads nearby rather than anything that looked like a fault.
If you are doing this: derive keys from the full path, not the basename. Then assert that every region in your plan appears in your output and every country you expect appears in your catalogue. Both failures were invisible at the file level and obvious the moment anything counted.
Each region has its own block grid
We divide each region into blocks so a phone can decode a small window rather than a whole country. Block width in longitude is derived from the region's own mid-latitude, to keep blocks roughly square on the ground.
That means the grids do not line up between regions. Switzerland's blocks are 0.219° wide; Baden-Württemberg's, immediately to the north, are 0.226°. We had written — in our own specification — that the grids agreed everywhere. They do not, and a client that computed one block coordinate globally would read the wrong block near every border, which is exactly where two regions are both in play.
If you are doing this: resolve block coordinates per file, using that file's own header. Or use one global grid and accept non-square blocks near the poles. Either is fine; assuming the first while implementing the second is not.
Memory: the shape of the data matters more than its size
Encoding India needed 26.7 GB of a 30 GB machine and spent most of its time swapping. The cause was not the volume of data but its representation — road geometry held as Python lists of coordinate tuples costs roughly 112 bytes per coordinate pair, against 8 bytes packed.
Switching to flat typed arrays brought the peak to 7.1 GB and produced byte-identical output, confirmed by checksum.
A related trap: on Windows Subsystem for Linux, /tmp is a RAM-backed filesystem. A node index written there consumes memory rather than disk, and takes the virtual machine down with it. Write scratch data to real storage.
What we did not fix
Coverage is what it is. Speed limit coverage varies by roughly 100× between countries. We publish what OpenStreetMap holds and show nothing where it holds nothing, rather than inferring a limit from road class — inference is only defensible in a handful of countries with strict, well-known legal defaults, and a confidently wrong number on a motorcycle dash is worse than a blank one.
We do not edit the source. If a limit is wrong, the fix belongs in OpenStreetMap, where it benefits everyone. We rebuild monthly, so a correction there reaches our riders within weeks.