In Part 1, I proved the core trick: DuckDB-WASM can query Overture Maps GeoParquet directly from S3 using HTTP range requests. No backend database. No ETL. Just a browser tab doing real SQL.

That part was fun.

The hard part came next: user experience.

When your “database” is a pile of Parquet files on the other side of the planet, performance is slow by nature. You can optimize the bytes, but you can’t wish away physics, request fanout, or cold caches.

So iteration 2 was about a different goal:

Make slowness legible and controllable, while staying cheap enough to leave running forever.

If you just want to click around first: https://maps.marchen.co

TL;DR (the pitch)

Business wins

What you can actually do in the UI

Technical wins

Net result: a thin client for the Overture Maps data lake where the query engine is the browser — honest about latency, still usable.

The UX Problem: Parquet-over-HTTP Doesn’t Feel Like a Database

A real database gives you consistent feedback loops:

That’s the mental contract people bring to “SQL.”

Parquet-over-HTTP in the browser is weirder:

So I stopped trying to make it “fast” and started trying to make it honest.

The New Mental Model: Three Caches, One Job

This iteration ended up with three distinct caching layers:

1) Edge cache: Which files matter?

The most important edge optimization is still file filtering — when you can do it.

In the “fancy” version of this project, the browser asks the edge:

…and the edge answers quickly because it keeps a spatial index of file bboxes built from Parquet metadata.

That index is keyed by {release}/{theme}/{type} and cached at the edge via the Cloudflare Cache API — no Durable Objects, no KV, no R2. The worker builds it lazily on first request (reading Parquet footers; tooling details are covered in Part 1), then caches it for a day in production (1 minute in dev). Subsequent requests are instant lookups.

The same stateless worker also provides:

That’s the “minimum viable edge”: make the browser’s SQL possible, without pretending the edge is a database.

2) Browser cache: What tables do I already have locally?

DuckDB tables in WASM are the local cache.

The mental model is simple:

  1. Pick a viewport you’re about to investigate
  2. Load the themes you need for that viewport (buildings, segments, addresses, etc.)
  3. Iterate inside the cached viewport: zoom/pan within it and re-render, paginate, and inspect — without re-downloading the world

Behaviorally, it works like this:

It’s not HTTP caching; it’s a stateful local database. That’s the point of DuckDB-WASM: exploration becomes cumulative instead of stateless.

3) Visual cache: Can the UI explain what’s happening?

The edge and the database can be “right” and the app still feels broken.

So the UX work here is about making state visible:

Product Shift: From “Places + Buildings” to “Any Theme, Any Type”

Part 1 was hardcoded around places/buildings.

Part 2 turned the viewer into a general Overture release browser:

This matters because it changes how you think about the app:

It’s not a demo.

It’s a thin client for a public data lake.

Performance Truth: Slow, Predictable, Cheap

This architecture is not fast.

It’s predictable.

Why it will always be slower than tiles

Vector tiles are precomputed for visualization.

This app is not a visualization-first pipeline. It’s an ad-hoc query engine:

That buys you a superpower (no backend, no ETL, no server bill), but the cost is latency.

Why it’s still worth it

Most data tools start with:

This starts with:

The edge isn’t there to compute your answers.

The edge is there to make your answers possible.

Visual Design for Dense Geometry: Opacity + Z-Order

Once you start mixing multiple layers, the map becomes unreadable fast:

The fix is boring and effective:

Bonus UX: Intersection Highlighting (and Why It’s More Than a Visual Toy)

Once you have multiple themes loaded, you inevitably ask:

“Are these layers actually aligned, or am I hallucinating a join?”

A small intersection/highlight mode turns that into a one-click check.

And once you have intersections… you can turn it into a discovery tool.

My Favorite Workflow: Find the OSM / Nominatim IDs That Intersect My Area

This is the moment the app stopped being “a map” and became a research tool.

Instead of guessing which OSM relation represents “the place I’m looking at,” I can load the admin boundaries (division_area) and click what I see. The UI immediately shows the canonical identifiers (OSM relation ids / records) that intersect my area.

That replaces a whole scavenger hunt across Nominatim, Overpass, and random wiki pages.

Why the Edge is the Right Place for “Metadata Compute”

There’s a specific sweet spot for the Cache API here:

Early versions used Durable Objects for this. Turns out, that’s overengineering. The Cache API does the same job: the worker builds the index on first request, caches it at the edge, and subsequent requests are instant. No persistent state to manage, no sqlite classes to migrate, no OOM issues in local dev.

The app delegates as much as possible to the edge, but it delegates the right thing:

This keeps the infra bill at ~zero.

What’s Next

I’m not pretending this replaces tiles.

But as a pattern, it feels important:

Stay tuned: search at the edge (without building a backend)

The next iteration I want to try is new kinds of search powered by the edge — not “run queries on the worker,” but push more discovery closer to the data:

Same rule as before: the edge coordinates, the browser computes.

The surprising part is how little code you need to make it feel coherent.

The app is slow.

And it’s still good.

Because it’s slow in a way the user can understand.


Part 1: SQL in the Browser: Querying 2.3 Billion Records with Zero Backend

Try the map: https://maps.marchen.co

Code: https://github.com/nikmarch/overturemaps-duckdb