I wanted a “how noisy is this neighbourhood, roughly?” map that I could pull up before picking a short-term rental, before agreeing to view a flat, or just out of curiosity when walking somewhere new. There are a few of these around, but they all either (a) hide the interesting bit behind an account, (b) are locked to one country, or (c) are essentially a screenshot of a PDF pretending to be a web map. I wanted something that loads instantly, works globally, respects my data, and lets me see the trade-off between “noisy road” and “quiet rail” without squinting.

noise.widgita.xyz is my take at that. It’s a zero-backend map - static files on nginx, PMTiles served with HTTP range requests, MapLibre GL on the client - with four noise source layers on top of OpenFreeMap: road, rail, industry, and aviation. Click anywhere and you get a 0–10 noise score computed directly from the tile the browser just fetched, not from whatever the renderer happened to paint.

The four layers

The layers are deliberately coarse. The goal isn’t acoustic modelling; it’s “give me an honest at-a-glance impression of what’s loud around here.”

  • Road - OSM-derived road noise, bucketed into four levels based on road class and speed. This is the biggest one in most cities.
  • Rail - same treatment for surface rail, which tends to be a much narrower but sharper corridor.
  • Industry - OSM industrial/landuse polygons. Lower fidelity, but useful as a “you may not want to live next to this” hint.
  • Aviation - approximate concentric rings around ~216 major airports worldwide. Not actual flight-path contours, just a “SFO is loud, the rings tell you how far that reaches.”

Every layer is clearly marked with its source (OSM or APPROX.) so you can’t accidentally mistake an aviation ring for a published noise contour.

The bit I kept iterating on: the layer panel

The original version had four checkboxes. That was enough to ship, but it fell apart the moment I had all four layers on in a dense European city - the magenta rail overlay painted on top of the orange road overlay painted on top of the blue aviation rings, and suddenly I couldn’t see the streets I was trying to look up. I’d turn layers off one at a time to “peel” down to the street grid, which is exactly the kind of friction a map shouldn’t have.

So the panel grew up. It now has three things per layer, in this order of “how often I touch it”:

  1. A checkbox - on or off, like before.
  2. A drag handle - reorder the stacking. Aviation on top by default (rings are big and hollow, so they look fine on top), then rail, road, industry. If you care more about road noise than rail where you’re looking, drag road to the top. Full keyboard support via Alt+ArrowUp / Alt+ArrowDown on the handle.
  3. An opacity slider - 0 to 100%. This is the one I missed the most. When three layers overlap on a dense block, pulling road down to 40% lets me see where the rail corridor actually is without losing the road context entirely. It turns “peeling” into “blending”, and makes the map much more useful for real exploration.

Both the ordering and the opacity preferences are persisted in localStorage, so the layout you fiddle with once is the layout you come back to. They’re also encoded into the URL hash, so a shared link reproduces the exact same visual state on the other end.

Scoring is done off the vector tile directly

One thing I’m quietly proud of: the 0–10 score under the pin is not derived from queryRenderedFeatures. MapLibre’s queryRenderedFeatures is intended for interactive use, not analysis - it reflects what’s currently painted, which can be empty or stale depending on zoom, viewport animation, tile eviction, etc. I kept getting zeros in Santa Clara and no data at all in Amsterdam because of this.

The scorer in noise-probe.js fetches the actual PMTiles archive over HTTP range requests, decodes the MVT locally with @mapbox/vector-tile + pbf, and samples the features that contain the clicked point. It’s independent of the renderer - the map can be mid-flight, tiles half-loaded, zoom in the wrong place, and the score is still correct because it comes from a deterministic re-read of the raw tile bytes. This is also what lets the score work everywhere, globally, not just where the current viewport happens to cover.

The stack

Nothing exotic:

  • MapLibre GL JS for rendering.
  • OpenFreeMap for the base map (dark and positron styles - swap via the theme toggle).
  • PMTiles for the noise vector tiles, served directly from nginx with gzip and Range enabled. No tile server process, no dynamic backend.
  • Static GeoJSON for the aviation rings, generated offline from a hand-curated airport list.
  • Nominatim for address search (rate-limited politely on the client).
  • URL hash + localStorage for state: hash for “share this exact view”, localStorage for “this is how I like my layer panel”.

The whole thing is ~60 KB of JavaScript, lazy-loads the map tiles the user actually needs, and doesn’t touch any backend I own beyond the static file server. Analytics is Umami - self-hosted, cookie-less, no personal data.

What’s next

The obvious next steps are a proper “compare two locations” mode (two pins, two noise profiles side by side), better night/day distinction for road noise (day-evening-night weighting where the OSM data supports it), and possibly an export-as-PNG button so people can actually use a map screenshot in a rental listing argument.

For now though, the layer panel alone made this go from “cool demo” to “a thing I actually open when I’m about to rent a flat.” Which is what I wanted.