An index of ~400 cafés inside Zürich's city boundary, built on OpenStreetMap and the city's own open data. No Google, no API keys for the map, and a nightly job that automatically updates and verifies data.
UX Design
UI Design
Backend Dev
Frontend Dev
DevOps
Every café gets a row, and every row carries an hours ribbon with a red line marking the current time. It turns "Is anything open?" into a shape you instantly understand, rather than ~400 strings you have to dig through individually. Opening hours come from OpenStreetMap, where they're written in a genuine little grammar like this Mo-Fr 07:45-16:30; Sa 09:00-16:00, with syntax for public holidays, seasonal rules and "sunset" — which we hand to a real parser rather than a regex we'd regret. 81% of cafés already had them when we did the initial import. A lot of data is still missing on OpenStreetMap though, only 23% have a phone number, which tells you something about what volunteers think is worth recording.
OpenStreetMap element IDs are not permanent. A mapper can delete a node and re-create it, or promote a point to a building outline. Six of our ~400 cafés are already outlines. Key your votes and editorial scores on node/123 and they silently orphan the first time someone tidies up.
So every café gets a cid of our own, minted once and kept forever, derived from a SHA-256 of the first OSM ID we ever saw so a lost registry regenerates identically. Each refresh reconciles in four passes: Exact ID match, then a fuzzy re-match by name and proximity within 90 m, then mint, then retire. Nothing is ever deleted, which means a café that closes keeps its record, its ID and its URL, because it might come back at a later date.
The fuzzy matcher scores names with Sørensen–Dice over character bigrams. Two distinct one-character names have no bigrams at all, so the denominator is zero and the score is 0/0 — NaN. Both callers rejected weak matches with if (sim < threshold) skip, and every comparison against NaN is false. The guard didn't fail closed, it failed open: Two unrelated cafés could be declared the same shop, one quietly inheriting the other's votes. A threshold that admits exactly what it exists to refuse is a good reminder to test the boundary, not the middle.
Café pages render per request on Netlify and sit in the durable cache under a tag per café. When an owner updates their page, we purge that one tag: Their change is live in seconds, no rebuild, and a cache hit bills no function invocation. The index and the static pages are prerendered the ordinary way.
Behind it: Astro as our frontend framework, Supabase as our database with row-level security for the community recommendations, MapLibre GL on swisstopo's vector tiles (free inside Switzerland, no key), and Keystatic as the editor, which commits straight to GitHub, so every editorial decision is a normal commit with a normal diff and a normal author.
A GitHub Action refreshes from the Overpass API every night. If OpenStreetMap returns the same data twice, the pipeline writes a byte-identical file — timestamps and all — so git diff --quiet exits early: No commit, no deploy, no build minutes. Getting that property right meant not stamping every record with today's date, which is the obvious implementation and the wrong one.
Where OSM is silent, the City of Zürich's licensed catering register fills the gaps: CC0, authoritative for addresses, and good for 103 missing postcodes and 92 missing street names. OSM always wins where both have an answer.
We built against Google Maps (Google Places API) first and removed it entirely almost immediately. Biggest factor was licensing: Google's terms let you display a rating and other information from their API but never store it, which makes an honest offline-capable index awkward to build and impossible to own. Another big factor: When we found a café with the wrong hours there was nothing useful we could do about it. Getting edits accepted on Google Maps is not instant and to be honest, we would just feed the giant more data. In OpenStreetMap you just fix it, and the fix belongs to everyone, the whole community of mappers and every open source project that makes use of the data.
You can read our announcement for a less technical post or just click the button below to check out cafezuri.ch, find your next coffee spot or contribute to the open mapper community. Enjoy!