ToiletNearest.com
A standalone automated public toilet at night, representing the intersection of technology and urban civic infrastructure
Behind the Data

How We Map 4 Million Toilets Using OpenStreetMap

Engineering TeamNovember 2024 10 min read All articles

OpenStreetMap has more than 4 million nodes tagged with amenity=toilets. That number grows by roughly 3,000 new entries per month, contributed by volunteers in 195 countries. ToiletNearest.com displays all of it - live, filtered, and presented in a way that is useful in the 30 seconds before a crisis. Here is exactly how that pipeline works.

What OpenStreetMap actually contains for toilets

Every toilet in OpenStreetMap is a "node" - a point with latitude and longitude, plus a set of key-value tags. The amenity=toilets tag marks the basic fixture; additional tags describe its characteristics:

  • fee=yes/no - whether a charge applies
  • wheelchair=yes/limited/no - accessibility level
  • opening_hours - in a consistent ISO format (e.g. Mo-Fr 08:00-20:00; Sa-Su 09:00-18:00)
  • baby_changing=yes/no - presence of changing facilities
  • unisex=yes - all-gender access
  • toilets:disposal=flush/pitlatrine/dry_toilet - type of disposal

The completeness of these tags varies enormously by region. Japan and the UK have extremely well-tagged toilets - often including operator names, phone numbers, and even bin information. Parts of central Africa and rural South America have toilets mapped but with only the bare amenity=toilets tag and nothing else.

The Overpass API: how we query the data

OpenStreetMap does not expose a standard toilet search API. What it provides is the Overpass API - a read-only service for querying OSM data using a structured query language (Overpass QL). Our backend sends queries like:

[out:json][timeout:25];
node["amenity"="toilets"](around:2000,51.505,-0.09);
out body;

This asks: "give me all toilet nodes within 2,000 metres of latitude 51.505, longitude -0.09, in JSON format, with a 25-second timeout." The Overpass API returns a JSON object with an elements array.

We query three mirror servers in parallel (overpass-api.de, overpass.kumi.systems, and maps.mail.ru's instance) with a 28-second abort signal. The first successful non-empty response wins. This redundancy means Overpass downtime on one instance does not break the user experience.

The seed dataset: filling the gaps

Overpass covers most of the world well for urban areas. But some regions - rural Australia, provincial Pakistan, smaller Pacific island nations - have poor OSM toilet coverage despite having real public facilities. Our seed dataset fills this gap.

The seed dataset contains 400+ manually verified toilet locations at high-value waypoints: major transport hubs, tourist landmarks, national park visitor centres, and busy urban squares. These are included in the API response when OSM coverage is thin (fewer than 10 results within the search radius).

Every seed location is verified from primary sources - government open data, venue websites, and in some cases direct contact with facility operators. Australia's seed data draws from the National Public Toilet Map (toiletmap.gov.au), a government-maintained dataset of 19,000+ facilities.

Caching and performance

Raw Overpass queries take 2-8 seconds. We cache responses at the API route level for 10 minutes using Next.js's next: { revalidate: 600 } option. This means the first visitor to a given location gets a live Overpass response; subsequent visitors within 10 minutes get the cached version. The trade-off - slightly stale data - is worth it for the performance gain.

The radius cap (currently 10km maximum) is important. Without it, a single request near a city boundary could ask Overpass for every toilet in a 10km radius, which would return thousands of nodes and stress the OSM infrastructure. The radius is set by the map zoom level: zoomed out gets a smaller radius, zoomed in gets the full 2km default.

When the data is wrong

It happens. A toilet appears on our map in a location where there is no toilet. Or a real toilet does not appear because nobody has mapped it in OSM yet. Or the hours are wrong.

The fix for all three is OpenStreetMap editing. Any OSM account holder can edit toilet data using the iD editor (web) or JOSM (desktop). Corrected data feeds through to our API within minutes of the next cache expiry. This is the fundamental advantage of open data over proprietary databases: anyone can fix a bug, globally, immediately.

We actively encourage this in our UI: every toilet marker shows an "Edit on OpenStreetMap" link that opens the node directly in the iD editor. When enough users report or correct a location, it improves data for every app that uses OSM - not just ours.

What we build on top of the data

Raw OSM toilet data is not directly presentable in a consumer product. We normalise the tag vocabulary (mapping fee=no, fee=free, and charge=no all to our internal is_free: true), parse the opening_hours string into structured next-open/closed states, and run a basic duplicate detection pass to prevent the same toilet appearing twice from slightly different map positions.

The result is a REST API at /api/toilets?lat=&lng=&radius= that returns a consistent JSON schema regardless of how the underlying OSM data is tagged - something any developer can integrate into their own application.