AngoraTek FR
← Back to Blog
Data Engineering 9 min read

Your Data Joins Overture on a UUID, Not a Name

The row that anchors this post came back from a single query against Overture Maps release 2026-09-23.1, run with DuckDB 1.5.6 on 2026-10-11: place e13e7209-a09a-4cf7-9d45-0532936fcc4c, named Ashley Store, taxonomy primary furniture_store, confidence 1.0, sitting inside a bounding box we drew around Lake Tahoe. That string of hex is a GERS identifier from the places theme, and everything this post argues follows from what that identifier lets a join do.

State the claim so a skeptic can swing at it. When you move your own tables onto Overture, the work is entity resolution, and the deliverable is a versioned join table that maps your row keys to GERS identifiers, not a wholesale replacement of the keys your pipeline already has. If the migration plan drops the OpenStreetMap node and way IDs the pipeline was born on, the result is a schema that looks migrated and a lineage nobody can reconstruct. You join. You do not replace.

Name matching is the dead end your migration plan is standing in

What you actually need is a join, and the sizes make the point. Release 2026-09-23.1 carries 81,455,423 place features and 4,688,229 divisions, and the Tahoe bounding box alone holds 228,935 buildings. Somewhere in there is a target for every row of a store master, an asset register, or a parcel table.

The ways teams fake that join fail in an instructive order. Name equality dies first, on brand chains: the same store name appears in two malls a few kilometers apart, and a string cannot tell you which. Name plus edit distance finds the neighboring mall almost as often as the target. A geohash bucket plus straight-line distance works until a shared parking lot puts two candidates meters apart, which in a box holding 228,935 buildings is a normal case, not an edge case. Exact matches on phone or website cover fewer rows than they need to, because plenty of records carry neither.

The conflict underneath is precision against coverage. A threshold tight enough to trust a machine match is loose enough to send a third of your rows to manual work. That trade is survivable, but only if you make it on purpose, per row, with a record of who decided. That is the requirement the winning mechanism is built around.

GERS gives every consolidated feature a key with a paper trail

Three schema facts, all checked against release 2026-09-23.1, carry the argument.

Every feature in the places, buildings, and divisions themes carries an id of type uuid. The format matters less than the minting: Overture assigns the identifier during consolidation, so it survives the source churn that rewrites everything else about the feature. Next to it sits a version counter, and in divisions that counter is per record: the county, locality, neighborhood, macrohood, and microhood subtypes each carry their own version field, so you can see that a boundary you joined against last release has moved without diffing a single geometry.

Then the paper trail. Every feature carries a sources array, and each entry names a dataset, a license, and a record_id. For a place whose dataset is OSM, that record_id is your old node or way identifier. Nothing in a migration has to lose it. This is the escape hatch back to OSM-derived provenance, and it is the reason the join-table approach wins the lineage argument your data engineer was going to start in month three.

One honesty note before the workflow: stable means stable by policy. Release 2026-09-23.1 itself is proof, and the limits section comes back to it with numbers.

Query a box, score the candidates, accept above a threshold, log the rest

Here is the workflow on the shape of problem this post promises. Take a retail chain's store master: a few thousand rows of name, street address, phone, and a geocoded point, keyed today to OSM ways because the pipeline was born there. The target is Overture places, pinned to release 2026-09-23.1. Run the whole join for one row, the store whose geocoded point lands within a few tens of meters of the Ashley Store feature at the head of this post.

Pin the release name in configuration before any query runs, because every identifier you are about to store means only what release 2026-09-23.1 says it means. Then stage the candidate pool. The bbox struct column prunes Parquet files by partition and rows by predicate without opening a single geometry, so the Tahoe pool costs 2.77 seconds on a laptop and contains 28,166 candidate places:

SELECT id,
       names.primary AS name,
       taxonomy.primary AS category,
       confidence,
       sources
FROM read_parquet(
  's3://overturemaps-us-west-2/release/2026-09-23.1/theme=places/type=place/*.parquet',
  hive_partitioning = 1
)
WHERE bbox.xmin >= -121.0 AND bbox.xmax <= -119.6
  AND bbox.ymin >= 38.7 AND bbox.ymax <= 39.5;

Score the pool against your row next, on signals that fail independently: straight-line distance between your point and the feature, agreement between your category and taxonomy.primary, name similarity, and any hard signal both sides carry, such as a phone number or a website. One caution comes straight from the sample. In the Tahoe pool, 896 of 28,166 places, about 3.2 percent, have no taxonomy.primary, so a rule that requires category agreement silently drops them. Category corroborates a match. It never vetoes one on its own.

Accept above a threshold. Our working defaults machine-accept a composite score of 0.92 or better when the nearest rival candidate sits at least 0.10 behind, and send everything else to review. Those numbers are starting points, not laws. The right cutoff for your table depends on what a wrong join costs you, which is precisely the discussion the review log exists to inform.

Log the rest, always. The workflow's output is a table:

join_gers_places
  your_key         the row key your pipeline already has
  gers_id          e13e7209-a09a-4cf7-9d45-0532936fcc4c
  release_id       2026-09-23.1
  score            0.0 to 1.0, the composite you computed
  reviewer_status  machine_accepted, human_accepted, or pending

The row that failed the threshold is not lost; it is a queue a human clears in a sitting. For the store at the head of this post, distance and taxonomy agreement settle the match, confidence 1.0 from Overture's own scoring sits in the row to cross-check against, and the phone check never has to fire. Nothing in the workflow needed fuzzy matching to survive, because GERS turned the problem from guessing into bookkeeping.

The quiet win lands upstream, in the feeds you chained together

Most teams migrating carry more datasets than they admit. A CRM extract, a vendor POI feed, and an OSM-derived cache each describe the same portfolio of stores under three private key systems, and the pairwise reconciliation of those feeds is the project that never ships.

Run the workflow above against each feed separately and the reconciliation collapses to a group by. Two rows from different feeds that resolve to the same GERS identifier are the same store. When the two disagree about operating status or opening hours, the disagreement becomes a row in a report with a release stamp and a score attached, instead of a hunch in a spreadsheet. Where you need the original source, sources[].record_id hands you the OSM record underneath to reconcile against. The chained datasets stop being a tangle because, for the first time, they share a key none of them issued.

Where the join still hurts, and the workaround for each hurt

Four costs show up in real migrations, and each comes with an attached workaround.

Stable means by policy, not physics. The release notes of 2026-09-23.1 document a change in UUID generation for base and building_part features, so those features were re-issued with new identifiers while places kept theirs. Anything keyed to a base or building_part identifier from an earlier release drifted, with no upstream edit to point at. The workaround is the discipline this post has been running all along: the join table is a cache, and it is versioned by release. Carry release_id in every row, re-score the types a release note touches, and diff the table between releases before anything downstream consumes it.

Addresses cannot join at all. The addresses theme ships 474 million features in release 2026-09-23.1 and the Overture docs mark it alpha, which includes no GERS identifiers yet. Address-keyed records, and most retail masters are, cannot resolve the way places do. Attach them instead through the building they sit in or the nearest place, keep the raw address string untouched for audit, and revisit the theme when identifiers land.

The false attach is the quiet failure mode. A radius query around a mall can return a dozen plausible candidates, and a row joined to the wrong feature is worse than a row joined to nothing, because the wrongness reads as truth downstream forever. The columns are the workaround. score and reviewer_status are mandatory, and machine acceptance additionally requires a hard-signal corroboration such as a matching phone number. The pending queue is a queue with a human behind it, not a folder for rows you would rather not look at.

Thresholds decay. Categories move, chains open and close storefronts, and the composite that was safe at the release you pinned can be wrong at the next one. The cache versioning from hurt one covers this too, on a schedule: re-score on every re-pin and let the diff, which costs one query against your own table, tell you which rows moved.

The decision: keep both keys, version the table

If your migration plan contains a step named "drop the OSM IDs," cut it. Pin release 2026-09-23.1, resolve your rows into GERS identifiers by the workflow above, and let the join table carry both key spaces with a release stamp and a reviewer status on every row. The identifiers are stable by policy and churn by release notes, the addresses have no identifiers yet, and your own scoring is the piece most worth auditing. None of that argues for name matching. It argues for joins with receipts.

GERS identifiers do not replace your keys. They give them somewhere to land.

AngoraTek builds geospatial data infrastructure, including the entity-resolution layer between your tables and Overture. Bring us the join that keeps stalling and we will score it with you.

Thanks for reading this far. If this sounds like the problem you're wrestling with, tell us about it. A principal reads every message.

Start a conversation