尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Vietnam Map Archive — georeferenced historical maps of Vietnam. SvelteKit + OpenLayers + Allmaps. Live at maparchive.vn
翻译准备中
正在加载完整 README。 以下为已保存的摘录。
Historical maps of Vietnam, put back in place. We take scans of old city plans,
pin them to real coordinates so they line up with the city as it is now, and
read the names and shapes printed on them — so a street name from an 1882
survey becomes something you can search for, and a building traced off a 1959
sheet becomes a shape with a location.查看完整 README ↗Historical maps of Vietnam, put back in place. We take scans of old city plans,
pin them to real coordinates so they line up with the city as it is now, and
read the names and shapes printed on them — so a street name from an 1882
survey becomes something you can search for, and a building traced off a 1959
sheet becomes a shape with a location. Then we link each one to other maps, to
its sources (documents, photos, studies, writings) and to the city today: a
machine proposes each link and a person decides. What VMA is, on one page: docs/CONCEPT.md.
Saigon in the French colonial period is where the work goes deepest; Hanoi, Huế and the rest of the country are in the archive too. A SvelteKit application over Allmaps, OpenLayers and Supabase, with an OCR and segmentation pipeline behind it, all of it human-reviewed before anything is published.
Version history: maparchive.vn/changelog
in plain language, CHANGELOG.md with the engineering detail.
Currently 7.4.
/trip/<id> (which is what printed QR codes point at).Everything above is a public read; contributing needs an account, and the pipelines need staff.
npm install
npm run dev # http://localhost:5173
The dev server talks to the real Supabase project, so a fresh clone gets the live archive read-only. Writes need the local stack (below).
npm run check # type-check — the primary gate, kept at 0 errors / 0 warnings
npm run lint # prettier --check . && eslint .
npm run test # 381 tests: 19 read-only Playwright smokes + 362 pure checks
npm run db:test # start a local Supabase stack and seed it
npm run test:write # write-path smokes, local stack only — refuses a non-loopback URL
npm run test is mostly not a browser suite. The pure checks ride the Playwright
runner because it is already installed, and they pin the things that fail
quietly: the two copies of the tile-density signal (one TypeScript for the
browser, one Python for the worker) against identical bytes, the grid arithmetic
that decides where a street from a printed index lands, the palette against WCAG
AA in both themes, and the predicate that decides whether an OCR run is allowed
to spend money.
Two map surfaces, and every tool is a mode of one of them:
/explore — the geographic surface. One OpenLayers map owned by MapShell, warped historical
sheets over a self-hosted vector basemap. Modes: browse, studio, story./scan — the pixel surface. ImageShell over a IIIF canvas, for work in a scan's own
coordinates. Modes: prepare, text, shapes, and the unlisted inspect. The names they
shipped under — triage, ocr, trace, review — still resolve, because they are in bookmarks
and in links already sent.Modes are query parameters rather than routes on purpose: the map, the basemap and the warped tiles stay loaded across a mode change instead of being torn down and rebuilt.
Source is layered, and the rule is enforced by lint rather than by review:
core → data → map → features → routes
core is pure (no OpenLayers, no Supabase), ui is leaf primitives with no
domain imports, $lib/server is blocked from the client by SvelteKit itself, and
routes stay thin — load and wire, no business logic. A feature may reach another
feature only through a declared seam.
| Route | What it is |
|---|---|
/ | The archive's front page: a slider between 1882 and today, featured sheets, and a live demo of the pipeline further down |
/catalog | Faceted catalogue with full-text search; inline map editing for staff |
/catalog/<slug> | One sheet's record page: the full tiled scan plus its title, date and places, server-rendered for crawlers and link previews. Addressed by name since migration 088; a uuid and every retired slug 301 here |
/catalog/place/[name] | The gazetteer: one page per attested place name |
/catalog/series, /catalog/series/[key], /catalog/series/[key]/[number] | A survey as one thing: every series, one series' index, and a page for each sheet it contains — held or not |
/explore | The map viewer — browse, studio, author stories. ?mode=annotate is the name Studio shipped under and still resolves |
/scan | The scan viewer — prepare, text, shapes (and the unlisted inspect) |
/trip/[id] | Story playback |
/contribute | How to help |
/contribute/georef | Georeference a sheet in the Allmaps Editor |
/admin | One staff console: ?tab=bulk · ?tab=scout · ?tab=status |
/changelog | Version history |
/directory | Every page in the archive, in one list |
/screens | Every design-system component, from fixtures — look here before building a second one |
/about, /blog, /blog/[slug], /login, /profile | Editorial and account pages |
Retired paths 301 in src/hooks.server.ts, including /view, /studio,
/create, /image, the old /contribute/* tools, /admin/*, and the
id-carrying /map/<id> and /place/<name>. Old links and printed references
still land somewhere real.
vmabeta.pages.dev 301s to maparchive.vn — one address for the site. Preview
deploys at <hash>.vmabeta.pages.dev are matched exactly and stay reachable.
Sign-in is Google OAuth — no passwords to store or leak. An account is
created on first sign-in. Roles live in profiles: user, mod, admin. Draft
sheets are readable by any signed-in user and never anonymously; publishing
requires that a sheet is actually georeferenceable.
Status is the only visibility model: draft → public → featured. Publishing a
sheet enqueues its own follow-up work — mirroring the annotation, tiling to R2 —
rather than leaving someone to remember it.
/catalog, staff) — paste a manifest URL from Gallica, the Internet
Archive, Rumsey, EFEO, Humazur. The server parses it, derives the canonical image-service URL and
the Allmaps ID, and probes the annotation server to see whether it is already georeferenced./admin?tab=bulk + scripts/bulk_upload_local.sh) — for our own scans. Rows, tiles
and thumbnail in one pass./admin?tab=scout) — scripts/scout/scout_*.mjs crawl external IIIF endpoints; a
moderator records why a candidate is worth having and imports it.Georeferencing happens in the Allmaps Editor, which has no webhook, so a
sync_allmaps job picks the finished work up. /admin?tab=status is where you
see what is stuck.
Nothing runs on the web server. A route that starts pipeline work inserts a
pipeline_jobs row and returns 202; a worker claims it.
source work/ocr/.venv/bin/activate
python work/worker/vma_worker.py --worker $(hostname) # poll forever
python work/worker/vma_worker.py --once # drain one job
The worker needs VMA_API_URL and VMA_WORKER_KEY and no database
credentials — claiming a job and reporting its results both go through
/api/pipeline/*. It takes whatever kinds the machine can run, so a worker left
running finishes what publishing enqueued.
work/ocr/, its own venv. Gemini Flash over IIIF tiles into ocr_extractions, proposed
and accepted at /scan?mode=prepare, then checked row by row at /scan?mode=text. Prompt changes
are gated on a measured quality baseline, not on how the output looks:
work/ocr/EVAL-BASELINE.md.work/MapSAM2/, a fine-tuned SAM2 fork. Runs on Colab, where the GPU is, using
the same worker with --kinds seg. Polygons land in footprint_submissions and are validated at
/scan?mode=shapes.The two meet on one full-image pixel grid, which is what lets an OCR label be joined to the shape it names.
PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY # the publishable key
SUPABASE_SERVICE_KEY # admin API routes only — the secret key
IA_S3_ACCESS_KEY, IA_S3_SECRET_KEY # Internet Archive upload
VMA_API_URL, VMA_WORKER_KEY # worker machines only, never the web app
.env.example has the shape. Worker keys are minted with
scripts/mint-worker-key.mjs.
npm run deploy # build + wrangler pages deploy
npx wrangler pages dev .svelte-kit/cloudflare # local Cloudflare preview
Three rules, each of which cost a run of dead builds to learn — the full account
is in docs/guides/deploy.md:
wrangler.toml, on purpose. One that carries pages_build_output_dir
replaces the dashboard's entire environment, secrets included. The R2 tile worker's own
worker/wrangler.toml is separate and fine.$env/static/private. Every environment that builds needs all of it present.import('path') fails the Functions bundle and publishes
nothing; use the node: prefix.A blank page in the minute after a deploy is edge propagation, not a bug. Wait, hard-reload, then debug.
Start with CLAUDE.md — the map of the repo and its rules,
written for both people and coding agents. Then:
| Doc | What it is |
|---|---|
docs/ROADMAP.md | The one tracker, open work only: the foundations pass, the OCR drain, Shapes, Search, Walk, Debt |
docs/roadmap-record.md | Frozen record of closed passes — what each one measured and what it turned up |
docs/guides/lessons.md | The rules paid for more than once, each with the failure that taught it |
docs/guides/architecture.md | The app: layering, every route, the map runtime, data, components, known debt |
docs/guides/db-guidelines.md | Schema conventions every migration follows, table by table |
docs/guides/api.md | Every server route, its auth class, its contract |
docs/guides/deploy.md | Cloudflare Pages, and the ten dead builds behind each rule |
docs/guides/design-system.md | Tokens, the CSS file map, the page template |
docs/guides/digitalize-guide.md | Operator guide for /scan?mode=prepare — including the failure modes that return plausible output while dropping data |
docs/guides/pipelines.md | OCR + MapSAM2 commands, and what not to re-attempt |
docs/guides/admin-tooling.md | Map editing, bulk upload, Scout, the R2 worker |
docs/plans/search-plan.md, docs/plans/platform-design.md | The temporal-fabric plan; the shared-platform proposal |
docs/plans/strategy.md, docs/plans/theory.md, docs/guides/user-guide.md | Vision and outward-facing prose |
docs/journals/ | Dated research notes |
work/MapSAM2/TECHNICAL.md, work/ocr/EVAL-BASELINE.md | Model notes; the measured OCR quality gate |
docs/archive/ | Frozen. Historical plans and the record of the August 2026 cleanup — not current |
SvelteKit 2 with Svelte 5 in legacy syntax ($:, export let, stores — not
runes; the house style is in src/lib/CLAUDE.md). TypeScript. OpenLayers 10 as the only
map engine, with @allmaps/openlayers warping the historical sheets. Supabase
for Postgres, auth and storage, with status transitions written as Postgres
functions rather than in the API. Cloudflare Pages for the app, R2 for IIIF tiles
and for the ~348 MB PMTiles basemap that spans Hanoi to the Mekong — no
third-party tile service, no API key, no usage policy to outgrow. Fonts are
self-hosted too.
The archive is worth more with more sheets in it and more names read off them.
/contribute says what needs doing; georeferencing needs no code, and
docs/guides/digitalize-guide.md is the operator guide for the reading pipeline.
Code: MIT. The archive's own data: CC-BY-4.0.
The code is permissive because the people who should be reusing it — libraries, universities, other archive projects — all have legal review, and a copyleft licence is the one that review rejects. The data carries an attribution requirement because it is the part that took two years: the georeferences, the names read off the sheets and checked by hand, the traced footprints, the gazetteer. Use it, commercially or not, modified or not; credit the Vietnam Map Archive Project and link back.
The scanned sheets are not ours to license. Each one belongs to the institution holding the paper, under that institution's terms, recorded per sheet and shown on its catalogue page. Check there before reusing an image.
The basemap is built from OpenStreetMap data (© OpenStreetMap contributors,
ODbL); the satellite imagery is Esri's. NOTICE.md has all of it
in full.
Questions: [email protected]