Skip to content

About

Interactive map of Japan's decorative manhole covers, built with MapLibre GL JS and OpenStreetMap data

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Manhole Japan logo

Manhole Japan

An interactive map of Japan's decorative manhole covers. In Japan almost every city and ward casts its own design (flowers, castles, festivals, local legends, Pokémon), and this map plots where they are.

Live demo

Screenshot

What it does

  • Clustered map of ~260 Poké Lids (ポケふた) across 27 prefectures, sourced from OpenStreetMap.
  • Filter by prefecture, type, and free-text search (design, town, Pokémon).
  • A stats panel and a per-prefecture bar chart that follow the current filter.
  • Click a cover for its photo, Japanese name, themes and source link.
  • Light / dark basemap, and a shareable URL (?pref=Miyagi&q=Lapras&id=…).
  • Add your own finds via a small JSON file. No build tooling required.

Stack

Static site, no backend. MapLibre GL JS with keyless OpenFreeMap vector tiles, vanilla ES modules, data as GeoJSON. A small Python pipeline builds the dataset. Deployed to GitHub Pages by Actions.

src/            vanilla JS modules (map, filters, stats, panel, url state)
data/
  covers.geojson      generated, this is what the map reads
  prefectures.geojson  simplified boundaries (for the prefecture filter)
  personal/*.json      hand-added observations, merged at build time
  sources/*.json       raw pulls, committed for reproducible builds
scripts/
  fetch_pokefuta.py   Overpass query -> data/sources/pokefuta_osm.json
  build_covers.py     normalise + assign prefecture + dedupe -> data/covers.geojson
  validate.py         CI gate (schema + photo checks)
  optimize_images.py  raw photos -> WebP
  smoke.mjs           headless jsdom wiring test

Develop

npm install
npm run data       # fetch + build + validate the dataset
npm run serve      # http://localhost:8777
npm run lint
npm run smoke      # headless functional test

Add a cover you've seen

  1. Put a photo at assets/photos/_raw/<slug>.jpg and run npm run images.
  2. Add an entry to data/personal/mine.json (copy data/personal/_template.json): coordinates [lon, lat], name_en, municipality, themes, and photo: "assets/photos/<slug>.webp" / photo_thumb: "…thumb.webp". Leave prefecture_en null. It's filled in from the coordinates.
  3. npm run data then npm run lint, then open a PR.

Add a cover from the site

Open + Add a cover (top of the panel) or the Identify a cover link in the footer. Pick a JPEG/PNG of a cover: the page reads its EXIF GPS, can OCR the text cast into it, and (once enough photos exist) runs a prefecture classifier, then gives you a ready-to-paste JSON block, two optimised WebP files, and the steps to open a pull request. Everything runs in your browser. The photo is never uploaded, and its GPS metadata is stripped from the files you download.

Origin recognition

src/recognize/analyze() combines four independent signals:

Signal How Notes
GPS EXIF coordinates → prefecture (point-in-polygon) + nearest known cover strongest; needs a geotagged photo
OCR PaddleOCR (ONNX, PP-OCRv5 "ch" model) reads the cast/painted text → matched against data/municipalities.json reads kanji reliably even in stylised cover art; the model's dictionary also covers hiragana/katakana, but that path is less tested
Classifier mobilenet_v3_small fine-tuned on contributed photos, run via ONNX in the browser shows "not enough data yet" until ~50 labelled photos exist, then trains automatically (train-model.yml)
Visual match DINOv2-small image embedding compared against data/embeddings.bin dormant until contributed photos exist (see below)

data/municipalities.json is built by scripts/build_gazetteer.py from geolonia/japanese-addresses (licence noted there).

OCR trade-offs worth knowing: @paddleocr/paddleocr-js is the official SDK from the PaddlePaddle/PaddleOCR monorepo (Apache-2.0), but it's still pre-1.0 and thinly maintained, so it's pinned to an exact version. Vertical Japanese text (common on stamped/circular cover art) hasn't been verified in a real browser yet, only horizontal stylised text.

Appending ?tool=identify to the URL opens the same engine as a standalone tool: drop a photo, read the three signal cards and the verdict, then hand off to the pre-filled contribution form.

Batch add + deduplication

?tool=batch (or the Batch button) takes several photos at once: each is analysed, rows are editable inline, and you get one JSON array for data/personal/mine.json plus a .zip of every WebP. Photos at (nearly) the same spot, or that look near-identical, are flagged as duplicates and skipped by default.

Deduplication combines GPS proximity (< 15 m) and visual similarity: a DINOv2-small image encoder (Apache-2.0, ~23 MB, loaded lazily from the Hugging Face CDN, so nothing large is committed) embeds the photo and compares it against data/embeddings.bin. Like the classifier, the visual library is empty until contributed photos exist; scripts/build_embeddings.py + .github/workflows/embed.yml regenerate it via PR. models/embed-model.json records the model, revision and licence.

Help classify a cover

Some contributed photos have no usable location: no GPS in the source image, and no publicly documented exact address (official "manhole card" locations are handed out on physical cards, not published online). ?tool=unclassify (or the Help classify button) lists them from data/unclassified.json; if you recognise one, fill in its name/prefecture/coordinates and it builds the same JSON block + publish steps as the normal contribution form. Nothing is uploaded automatically; a human still opens the pull request.

Gallery

?tool=gallery (or the Gallery button) is a clean grid showcase of every cover in the current filter that actually has a photo. Poké Lids are location-only (OSM data, no image rights to show), so they're skipped automatically rather than rendering broken thumbnails. Click a photo for the full-size version; click a caption to jump straight to that cover on the map.

Data & attribution

Data Source Licence
Poké Lid locations OpenStreetMap via Overpass ODbL 1.0
Prefecture boundaries dataofjapan/land as upstream
Basemap OpenFreeMap / OpenStreetMap ODbL 1.0
Personal observations & photos Sofiane Beloucif see PHOTO_CREDITS.md
Community photos (commons category) Wikimedia Commons contributors CC-BY-SA / CC0, credited per entry in PHOTO_CREDITS.md

Photos in this repo are the author's own work or Creative Commons / public domain, credited per entry. The commons category is picked by hand for generic municipal motifs only (flowers, fruit, bridges, folklore); a free licence on a photo does not clear the copyright of a third-party character depicted in it, so no Poké Lid design is sourced this way; see "Origin recognition" above for why those stay location-only. The code is MIT (LICENSE); generated data files derived from OpenStreetMap remain under ODbL.

This is a fan project. It is not affiliated with The Pokémon Company, Nintendo, or the GKP / Japan Sewage Works Association.

Roadmap

  • v2: GKP manhole-card dataset (scraper + ~1000 more covers), list/grid view, prefecture choropleth.
  • v3: personal "visited / card collected" layer (localStorage), OSM enrichment, flake.nix dev shell, FR/JA UI.

About

Interactive map of Japan's decorative manhole covers, built with MapLibre GL JS and OpenStreetMap data

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages