Overture's Schema Is Readable in an Afternoon
Release 2026-09-23.1 of Overture Maps dropped the categories column from every one of the 81,455,423 place records in the dataset, and any pipeline that read it stopped running the day the release went live. The columns that replaced it are better designed than the one they buried. The removal still opens this walk through the schema, because it settles the ground rules before we touch a single column name: Overture is excellent data, and it is versioned like software, churn included.
The claim this post sets out to earn: an engineer who knows SQL but has never opened Overture can read every column that will drive an engineering decision in one afternoon with DuckDB. We ran the queries below on October 11, 2026, with DuckDB 1.5.6 and its spatial and httpfs extensions, against release 2026-09-23.1, and every number in this post descends from that run. The schema is much larger than your project and flatter than it looks. A short list of column names carries the decisions, and the rest is reference you DESCRIBE only when a bug sends you there.
Pin the release ID, because the columns move
The categories removal is the proof, so take the operational lesson before the schema tour. The release ID lives in your parquet path as a literal string, and keeping it there makes every count and join reproducible. Pinning costs nothing today and buys a known diff tomorrow, because Overture ships monthly and a rebuild six months out can rename a column you had scheduled to read forever. It is the same argument this blog made about map providers in August: the seam you build before you need it is the one that holds. When a schema change lands anyway, the cheapest question in your toolkit asks the storage layer what it actually contains:
DESCRIBE SELECT * FROM read_parquet(
's3://overturemaps-us-west-2/release/2026-09-23.1/theme=places/type=place/*.parquet',
hive_partitioning = 1
);
Run that once per type, for whichever release you pin. Against release 2026-09-23.1 it prints 18 top-level columns for places, 24 for buildings, and 21 for transportation segments. Those three tables are where most geo projects live. We take them in order, one example row at a time, starting with the theme everyone opens first.
A place is joinable before it is trustworthy
Here is the example row for this post, a furniture store at Lake Tahoe. The predicate runs on the bbox struct, not on geometry, so the query prunes parquet files before parsing anything:
SELECT
id,
names."primary" AS name,
taxonomy."primary" AS category,
confidence,
websites
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
AND names."primary" = 'Ashley Store';
What came back from release 2026-09-23.1:
| column | value |
|---|---|
| id | e13e7209-a09a-4cf7-9d45-0532936fcc4c |
| name | Ashley Store |
| category | furniture_store |
| confidence | 1.0 |
id is a UUID designed to stay stable across releases, and GERS is the mechanism behind that promise. Treat it as the foreign key of your whole project: place to building, or this month's release to last month's. confidence is the trust dial. A 1.0 is as confident as the column gets. The long tail of generated and machine-matched places lives far below it, and where you draw the rendering line is a product decision the schema will not fake for you. names is a struct too, carrying the primary label, a common alternate, and region rules.
taxonomy is the struct the categories column became: one primary category, a hierarchy list of the ancestors above it, and an alternates list alongside. basic_category sits beside it, and the two vocabularies are not interchangeable; the last section of this post is about the rows where the primary is missing entirely.
The columns that decide what you can do with a row come packaged as arrays. websites, emails, phones, socials, and addresses are all lists, because real businesses publish several of each. brand carries a Wikidata reference plus names, so a franchise chain collapses to one identity without string matching. sources[] is the legal and freshness contract: each entry carries a license and an update_time, which together decide whether you may republish the row and how stale it already is. operating_status closes the loop, because a perfectly confident furniture store that shut its doors last year is still bad data on your map.
Height turns building polygons into a dataset
The same Tahoe rectangle holds 228,935 building polygons in release 2026-09-23.1, and counting them took 37.1 seconds on our run. That timing is the schema's quiet trick. The predicate ran on the bbox struct column, which prunes parquet row groups before any geometry is parsed. Filter on bbox and leave geometry alone, and most of your Overture queries inherit the same speed.
Vertical structure is what turns those polygons into engineering material. height and min_height feed extrusion and shadow work. num_floors and is_underground cover the buildings that publish floors but not meters. When a floor count exists without a height, we fill the gap at three meters per floor, a heuristic our own code comments admit to. subtype and class carry the classification, and the class list keeps growing. Release 2026-09-23.1 added a shelter class, per the release notes.
Renderers get their own contract in the facade and roof attributes: facade_color, facade_material, roof_shape, roof_material, roof_orientation, roof_height, and their neighbors. Pipelines that only move geometry can ignore all of them. That asymmetry is the argument for DESCRIBE-first work: load the columns your workload pays for, and skip the styling contract if nobody renders.
Segments carry rules, not just shapes
106,242 transportation segments sat in the same rectangle, and that count came back in 18.0 seconds. If places is the theme people try first, segments is the one that changes their architecture. A segment records what traffic is allowed to do with a piece of road, not merely where the road lies.
subclass and subclass_rules are the pair to read together. The subclass names the kind of road, and the rules attach conditions to that kind, with value and between entries scoping where each condition applies. speed_limits arrive as min/max pairs with an explicit unit, so nobody's toolchain improvises a conversion. access_restrictions decides who may go where. road_surface and road_flags describe the physical and administrative state of the pavement. routes carries signed network membership (name, network, ref) for shields, and destinations holds the signage a router should repeat at an exit.
The geometry waits at the bottom of the column list, and it is the only part you would have guessed from the theme's name. Everything above it is a rules table.
896 Tahoe places have no primary category, and your code should expect them
Now the limit we promised. Within the 28,166 places our bbox filter returned from release 2026-09-23.1, 896 have no taxonomy."primary" at all, 3.2% of the sample. Code that reads the primary usually assumes NULL means something like "unclassified business", when it actually means "column absent", and an absent column poisons downstream filters silently. Write the fallback before the first join trusts a category:
SELECT
id,
COALESCE(taxonomy."primary", basic_category) AS category
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;
The fallback works because basic_category is populated on those rows, but it is a different vocabulary, so record which column fed each result and keep your category-level aggregates able to say so. Fairness cuts the other way too: 3.2% is one rectangle of one release. Measure your own share before you inherit ours.
Two more limits we pin rather than hide. The addresses theme still carries an alpha flag in the documentation, its 474 million features carry no GERS identifiers yet, so addressing work should not anchor on it. And the UUID generation change in this same release churned some building_part identifiers, per the release notes, which means every release upgrade re-checks your joins against the new IDs.
The afternoon, in order
If you start an Overture pipeline this quarter, here is the order we would use. Pin the literal release ID in every parquet path. DESCRIBE the three types before the first SELECT gets written. Put the COALESCE between the taxonomy and every consumer on day one. Store the release ID beside every row you persist. None of that is deep Overture knowledge. All of it fits in one afternoon, and Overture publishes a build-a-map example that takes your second hour, from DuckDB query to tippecanoe tiles to a MapLibre map.
The schema is not the hard part of an Overture project. Reading it late is.
AngoraTek helps teams load Overture releases and survive their schema changes. Talk to us if your pipeline should have been readingtaxonomy in September.
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