AngoraTek FR
← Back to Blog
Tutorials 8 min read

Overture's Whole Planet Answers COUNT(*) in 0.37 Seconds

81,455,423 places came back in 0.37 seconds. The question was asked of the entire places release 2026-09-23.1 from Overture Maps, sitting as GeoParquet in an S3 bucket nobody at AngoraTek operates, and not one place record crossed the network to answer it. DuckDB read parquet footers, summed the row counts recorded in metadata, and stopped there.

Treat that timing as a curiosity with a mechanism, not a selling point. The claim this post defends with measurements is more ordinary and more useful. For regional work you do not need a download pipeline for Overture: DuckDB prunes the planet with a bounding-box predicate before it touches a single geometry. What remains of the extract script is one COPY statement. We will run one example the whole way through, a regional extract around Lake Tahoe. Every number below was measured on 2026-10-11 with DuckDB 1.5.6 locally against release 2026-09-23.1, so you can reproduce the lot.

The setup is four lines and a release ID

Overture publishes each release as GeoParquet under one S3 prefix, in hive-style folders such as theme=places/type=place/*.parquet. The folder names behave as real columns once DuckDB is told they exist, which is the job of hive_partitioning. Two extensions and you are in business:

INSTALL spatial;
INSTALL httpfs;
LOAD spatial;
LOAD httpfs;

httpfs gives DuckDB its S3 reader. spatial installs the geometry types and predicates the final step of the workflow wants. Overture ships a fresh release ID every month, so pin the one you test against in the path string rather than pasting whatever is new that week, because counts change underneath you otherwise. Overture's documentation keeps an official DuckDB guide and a workshop at labs.overturemaps.org. Nothing in this article needs more than the four lines above.

The 0.37-second count never reads a place record

A count with no filter exercises parquet metadata alone:

SELECT count(*) AS places_on_the_planet
FROM read_parquet(
    's3://overturemaps-us-west-2/release/2026-09-23.1/theme=places/type=place/*.parquet',
    hive_partitioning = 1
);

DuckDB fetches each file's footer, reads the row counts recorded there, and adds. The network carries footers rather than rows, which is how 81 million places cost under a second. I now treat that predicate-free count as a reachability check rather than a result: it proves the bucket, the release ID, and the partitioning all line up before any expensive query runs. It has kept that job in the Tahoe workflow, run once, at the start, as the smoke test.

Read the 0.37 seconds for what it is. Metadata arithmetic. Never quote it as the speed of querying Overture.

Only the bounding box prunes

Every Overture feature ships with a bbox struct, xmin through ymax. A predicate against that struct prunes at parquet's own level: row groups whose statistics put their boxes outside your region get skipped whole, without one geometry being deserialized. The statistics live in file metadata DuckDB already knows how to read, so the check costs a comparison instead of a download. Every timing in this article rests on that mechanism.

Our first version of the Tahoe filter skipped the box and went spatial immediately, ST_Contains against a lake polygon. It was the more correct predicate and the worse plan: a geometry predicate must pull and reconstruct each candidate geometry, so it prunes nothing on disk and pays for the full read. The bbox predicate does the pruning, and geometry can be applied afterwards on the survivors:

SELECT count(*) AS places_in_tahoe_bbox
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;

Comparison operators, not spatial functions: this counts places lying wholly inside the box. It returned 28,166 places in 2.77 seconds on a warm connection. Where your boundary is not a rectangle, the workaround is two stages: bbox predicate first to prune, then ST_Intersects on the survivors to be exact. The first stage decides the bill the second stage pays.

The counts quote the price of the extract

Counting each theme before copying anything is how you learn what the extract will cost. The same bbox predicate over the buildings theme, theme=buildings/type=building, returned 228,935 buildings in 37.1 seconds. Over transportation segments, theme=transportation/type=segment, it returned 106,242 segments in 18.0 seconds. Everything measured for this article, warm connection, same laptop, release 2026-09-23.1, fits one table:

QueryResultReal time
Places, whole planet, no filter81,455,4230.37s
Places, Tahoe bbox28,1662.77s
Buildings, Tahoe bbox228,93537.1s
Transportation segments, Tahoe bbox106,24218.0s
The buildings number deserves its honesty note. The bbox pruning is identical in shape to the places query, and the time differs by a factor of thirteen. I did not dissect which row groups carried the extra mass, and I will not invent the cause; what the number establishes is the range. Places answered in under three seconds, buildings took about half a minute, and that spread is what you carry into any conversation where someone assumes all Overture themes behave the same.

Put the table in front of a real meeting and it changes shape. Your municipality asks for its building layer as GeoJSON, and someone suggests scripting a download of the Overture buildings theme because the file will be big. Count first: 228,935 buildings inside the Tahoe box, counted in 37.1 seconds. The measured count converts a vaguely threatening mega-download into a bounded extract, and it does so with a number anyone in the room can rerun.

An extract nobody will re-query is not worth its bandwidth. The counts above said the Tahoe sets were small enough to copy in one bbox pass per theme, so we did. Release 2026-09-23.1 ships places with schema v2.0.0, where the old categories field became basic_category plus the richer taxonomy, and the query selects both:

COPY (
    SELECT id, geometry, confidence,
           names.primary AS name,
           basic_category,
           taxonomy.primary AS category_primary,
           bbox
    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
) TO 'tahoe-places-2026-09-23.1.parquet' (FORMAT PARQUET);

From then on the questions run against the local file:

SELECT name, confidence, category_primary
FROM 'tahoe-places-2026-09-23.1.parquet'
WHERE category_primary = 'furniture_store';

Ashley Store, a furniture shop carried with confidence 1.0, is in those rows, with id e13e7209-a09a-4cf7-9d45-0532936fcc4c if you want to check the record yourself. Data-quality questions get cheap here too. Counting places with no primary category is one line against the copy:

SELECT count(*) AS places_without_primary_category
FROM 'tahoe-places-2026-09-23.1.parquet'
WHERE category_primary IS NULL;

896 of the 28,166 Tahoe places, 3.2 percent, returned no taxonomy.primary at all, an honest gap in the source data that this kind of one-line probe surfaces without another bucket round trip. That loop is what the extract earns: each pass against S3 restarts the network tax, while the local copies answer without a bucket in the loop.

What this recipe will not do

Four limits, each with its workaround.

First reads are slow. The 2.77-second figure came from a warm connection. A first pass at this bucket fetches parquet footers through S3GetObject one file at a time, latency dominates, and DuckDB's httpfs cache only helps after the first touch. Expect the initial run of any of these queries to cost several times the warm figure. Keep a persistent DuckDB database file so caches and copies outlive the CLI session, and do repeated work against the local extract.

The 0.37-second planet count is not a throughput claim. It is a sum of footer values. Anyone quoting it as proof that Overture answers whole-planet questions in under a second is quoting the packaging, not the data.

Only bbox prunes. The failed lake-polygon attempt above is the general rule, not an exception: no geometry predicate prunes parquet. A region defined by an arbitrary polygon always pays the two-stage price, so budget for it before promising that stage's latency.

DuckDB is an analytics engine, not a serving database. The numbers above are single-session analytics timings, and none of them makes DuckDB the spatial backend for an API that serves maps to real users. PostGIS and document stores are where that conversation happens. We take the storage choice up in the follow-up on where an Overture extract should live, separate from the query-in-place recipe here.

The decision fits one sentence: if your region of interest fits in a bounding box, query Overture in place and count before you copy. Keep the extract for anything you will ask more than once. If you need the whole planet, or serving latency, the download script and the database come back on the table. 81,455,423 places are already sitting in the bucket, pruned to 28,166 by one WHERE clause. The planet is a table. Most projects only ever need a few boxes of it.

AngoraTek works with Overture and the rest of the open geodata stack. Talk to us when your map data question has outgrown the download script.

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