Query Overture Maps with DuckDB spatial and GeoParquet in Python

Pull Bristol's 144,965 buildings and 20,727 places straight from the Overture Maps GeoParquet release with DuckDB, run spatial SQL joins and H3 aggregation, load the result into GeoPandas, write GeoParquet 1.1, and time it against Shapefile, GeoPackage and FlatGeobuf.

Level
Intermediate
Tested with
Python 3.12
Source code
Download scripts

Key takeaways

  1. Overture publishes every theme as GeoParquet on S3 and Azure. DuckDB reads it with a bbox filter, so you download only the row groups that cover your area: 144,965 Bristol buildings came out of a 2.5-billion-building, 277 GB theme in 3.4 s.
  2. Use Overture's STAC index (collections.parquet) to pick the files first. Counting Bristol's buildings took 4.1 s against the one file that covers it, and 62.5 s when DuckDB had to check all 512 files.
  3. DuckDB spatial does the GIS work in SQL: footprint areas in British National Grid, a point-in-polygon join of 20,727 places against 144,965 buildings in 0.05 s, and H3 hexagon summaries in 0.11 s.
  4. GeoParquet beat the GDAL formats on size and full reads: 13.5 MB against 48.8 MB for GeoPackage and 193.2 MB for a Shapefile. GeoPackage and FlatGeobuf were still faster for a small bbox read, thanks to their spatial indexes.
  5. Licences differ by theme: buildings and water are ODbL, places are CDLA-Permissive-2.0, Apache-2.0 or CC0 by source. Keep the per-record source and licence columns and credit OpenStreetMap and Overture.

Before you start

  • Comfortable with GeoPandas basics (see Getting started with GeoPandas) and a little SQL
  • Python 3.10+ with duckdb, geopandas, pyogrio, pyarrow, shapely, pandas and matplotlib
  • An internet connection: the data is read from Overture's public bucket, with no account or API key

Short answer: to query Overture Maps in Python, pip install duckdb, load the spatial and httpfs extensions, and point read_parquet() at Overture’s public GeoParquet files on S3 (s3://overturemaps-us-west-2/release/<release>/theme=…). Filter on the bbox column (bbox.xmin <= east AND bbox.xmax >= west …) so DuckDB fetches only the row groups that cover your area. Then COPY the result to a local GeoParquet file. For speed, first use Overture’s STAC index to choose the few files that overlap your box. You don’t need an account, an API key or the overturemaps command-line tool.

Overture Maps is one of the largest open map datasets: 2.5 billion buildings, 81 million places, and roads, water and boundaries for the whole planet, refreshed every month. It is published only as GeoParquet in cloud storage, so you can’t open it whole in a desktop GIS. You query it. DuckDB is the tool for that. It runs inside your Python process, reads Parquet over HTTP, and its spatial extension speaks PostGIS-style SQL.

This is part 1 of the two-part series Cloud-native vector GIS with Python. We pull central Bristol out of the latest Overture release, analyse it with spatial SQL, hand it to GeoPandas, and save clean GeoParquet. In part 2 we turn those files into PMTiles vector tiles and a MapLibre GL JS web map on static hosting.

You will learn how to:

  1. find the latest Overture release and the files that cover your area from its STAC index;
  2. download buildings, places and water for a bounding box with DuckDB, without the rest of the planet;
  3. read Overture’s sources and licences for each feature;
  4. run spatial SQL: areas in British National Grid, a point-in-polygon join and an H3 hexagon summary;
  5. move the results into GeoPandas and write GeoParquet 1.1 with the right metadata;
  6. compare GeoParquet, FlatGeobuf, GeoPackage and Shapefile on size and speed.

All outputs below are real results from overture_duckdb.py, run on 19 October 2026 on Windows 11 with Python 3.12.0, DuckDB 1.5.6, GeoPandas 1.2.0, pyogrio 0.13.0 (GDAL 3.12.4) and pyarrow 26.0.0.

Set up

Terminal window
pip install duckdb==1.5.6 geopandas==1.2.0 pyogrio==0.13.0 pyarrow==26.0.0 shapely==2.2.0 matplotlib requests pytz

DuckDB downloads its extensions on first use. spatial adds the geometry type and ST_ functions, httpfs reads s3:// and https:// URLs, and the community h3 extension adds hexagons:

overture_duckdb.py
import duckdb
con = duckdb.connect()
con.sql("INSTALL spatial; LOAD spatial; INSTALL httpfs; LOAD httpfs; INSTALL h3 FROM community; LOAD h3;")
con.sql("SET s3_region = 'us-west-2'") # the bucket is public: no credentials needed

We use a 9.7 × 8.9 km box over central Bristol, from Leigh Woods in the west to St George in the east:

BBOX = (-2.66, 51.42, -2.52, 51.50) # xmin, ymin, xmax, ymax in lon/lat

Step 1: find the latest release and the files you need

Overture publishes a STAC catalogue. Its latest field names the current release, so the script never hard-codes a date. Each release also has a collections.parquet file. It is itself GeoParquet, with one row per data file and its bounding box, row count and size:

import requests
catalog = requests.get("https://stac.overturemaps.org/catalog.json", timeout=60).json()
RELEASE = catalog["latest"]
con.sql(f"""
CREATE TABLE stac AS
SELECT collection, num_rows, assets.aws."file:size" AS bytes,
assets.aws.alternate.s3.href AS s3_url, bbox
FROM read_parquet('https://stac.overturemaps.org/{RELEASE}/collections.parquet')
""")
xmin, ymin, xmax, ymax = BBOX
OVERLAPS = f"bbox.xmin <= {xmax} AND bbox.xmax >= {xmin} AND bbox.ymin <= {ymax} AND bbox.ymax >= {ymin}"
def files_for(collection):
return [r[0] for r in con.sql(
f"SELECT s3_url FROM stac WHERE collection = '{collection}' AND {OVERLAPS}").fetchall()]
Output — release and index
latest release: 2026-09-23.1 (schema 2.0.0)
themes: addresses, base, buildings, divisions, places, transportation
collection files features total_gb files_over_bristol
building 512 2533842612 276.9 1
place 16 81455423 11.0 1
water 128 66204219 28.6 2

The latest release on 19 October 2026 is 2026-09-23.1, schema 2.0.0. Overture’s files are sorted spatially, so Bristol’s buildings sit in just 1 of the 512 building files (277 GB in total).

The same OVERLAPS condition works on the data files too, because every Overture row has a bbox struct column. Parquet keeps min/max statistics for each row group, so DuckDB skips row groups that can’t match. Here is what choosing the files first is worth:

n_stac = con.sql(f"SELECT count(*) FROM read_parquet({files_for('building')}) WHERE {OVERLAPS}").fetchone()[0]
glob = f"s3://overturemaps-us-west-2/release/{RELEASE}/theme=buildings/type=building/*.parquet"
n_glob = con.sql(f"SELECT count(*) FROM read_parquet('{glob}') WHERE {OVERLAPS}").fetchone()[0]
Output — whole theme vs STAC-pruned
STAC-pruned file list, bbox filter : 144,965 buildings in 4.1 s
all 512 building files, bbox filter : 144,965 buildings in 62.5 s (15x slower)

Both queries give the same answer, but the glob has to open the footer of every one of the 512 files to read its statistics. The STAC index does that work once for everyone. These timings are over a home broadband link to us-west-2. Your times will vary with distance and bandwidth, but the ratio holds.

Step 2: download Bristol to local GeoParquet

Now we select the columns we want and COPY the result to a local Parquet file. Overture’s schema nests a lot: names.primary holds the name, and sources is a list of structs that records which dataset supplied which property, under which licence. An empty property means the whole feature came from that source.

queries = {
"buildings": f"""
SELECT id, names.primary AS name, subtype, class, height, num_floors,
list_filter(sources, s -> s.property = '')[1].dataset AS source,
list_filter(sources, s -> s.property = '')[1].license AS licence,
list_filter(sources, s -> s.property = '/properties/height')[1].dataset AS height_source,
geometry
FROM read_parquet({files_for('building')})
WHERE {OVERLAPS}""",
"places": f"""
SELECT id, names.primary AS name, basic_category, taxonomy.hierarchy[1] AS category_group,
round(confidence, 3) AS confidence,
list_filter(sources, s -> s.property = '')[1].dataset AS source,
list_filter(sources, s -> s.property = '')[1].license AS licence, geometry
FROM read_parquet({files_for('place')})
WHERE {OVERLAPS}""",
# Water polygons can be huge (the Severn estuary), so clip them to the box.
"water": f"""
SELECT id, names.primary AS name, subtype, class,
ST_Intersection(geometry, ST_MakeEnvelope({xmin}, {ymin}, {xmax}, {ymax})) AS geometry
FROM read_parquet({files_for('water')})
WHERE {OVERLAPS} AND ST_GeometryType(geometry) IN ('POLYGON', 'MULTIPOLYGON')""",
}
for name, sql in queries.items():
path = DATA / f"bristol_{name}_raw.parquet"
con.sql(f"COPY ({sql}) TO '{path.as_posix()}' (FORMAT parquet, COMPRESSION zstd)")
con.sql(f"CREATE OR REPLACE TABLE {name} AS SELECT * FROM '{path.as_posix()}'")
Output — download
buildings 144,965 features 13.3 MB 3.4 s
places 20,727 features 0.9 MB 5.3 s
water 161 features 0.1 MB 9.5 s

That is about 14 MB on disk for a whole city centre, in seconds. In schema 2.0.0, places no longer have a categories column. Use basic_category (for example cafe) and taxonomy.hierarchy, whose first element is the top-level group (for example food_and_drink).

Map of central Bristol showing about 145,000 building footprints coloured by subtype: mostly orange residential terraces, grey buildings without a subtype in the centre, red commercial buildings around Broadmead and Cabot Circus, and the River Avon and Floating Harbour in blue
All 144,965 Overture buildings in the box, coloured by subtype. Two-thirds are tagged residential; 30.5% have no subtype at all, mostly in the city centre.

Step 3: check sources and licences

Before you analyse or publish anything, find out where the data came from. The source and licence columns we extracted make that one GROUP BY:

Output — sources and licences
theme source licence n
buildings OpenStreetMap ODbL-1.0 140161
buildings Microsoft ML Buildings ODbL-1.0 4804
places meta CDLA-Permissive-2.0 10935
places Microsoft CDLA-Permissive-2.0 5060
places Foursquare Apache-2.0 4153
places AllThePlaces CC0-1.0 525
places DAC CDLA-Permissive-2.0 27
places PinMeTo CDLA-Permissive-2.0 27

In Bristol, 97% of building footprints come from OpenStreetMap and the rest from Microsoft’s machine-learned buildings. Both are under the ODbL. Places are a mix: Meta and Microsoft under CDLA-Permissive-2.0, Foursquare’s open places under Apache-2.0, and AllThePlaces under CC0.

Output — heights and place categories
height_from n median_height_m
(no height) 125192 NaN
Microsoft ML Buildings 19626 4.6
OpenStreetMap 147 3.0
category_group n median_confidence
services_and_business 6442 0.80
shopping 3271 0.85
food_and_drink 2424 0.94
(none) 1338 0.85

The output shows two quality caveats. 86% of buildings have no height, and almost all the heights that exist are Microsoft’s estimates, attached to OpenStreetMap footprints. Use them with care, for example in a 3D view. Places carry a confidence score from 0 to 1. Food and drink places have the highest median, at 0.94.

Two bar charts. Left: places by source, coloured by licence, with meta 10,935, Microsoft 5,060, Foursquare 4,153 and AllThePlaces 525. Right: places by top-level category, led by services and business with 6,442, then shopping with 3,271 and food and drink with 2,424
Overture places in the box by source (colour shows the licence) and by top-level taxonomy group.

Step 4: spatial SQL in DuckDB

DuckDB spatial’s functions are named like PostGIS’s. Overture geometry is in WGS 84 longitude and latitude (OGC:CRS84), so we project to British National Grid (EPSG:27700) to measure areas in square metres:

con.sql("""
ALTER TABLE buildings ADD COLUMN area_m2 DOUBLE;
UPDATE buildings SET area_m2 = round(ST_Area(ST_Transform(geometry, 'OGC:CRS84', 'EPSG:27700')), 1);
""")
Output — footprint areas
buildings footprint_km2 median_m2 max_m2
144965 13.15 58.0 30151.0
name subtype area_m2
Brunel Building medical 30151.0
Demolished site NaN 27651.0
The Pavilions commercial 16290.0
IKEA NaN 12448.0
BRI Queen's Building medical 11060.0

Buildings cover 13.15 km² of the 86 km² box. The median footprint is 58 m², typical of Bristol’s terraces. The largest is the Brunel Building at Southmead Hospital. The second largest is mapped in OpenStreetMap as a “Demolished site”, a reminder that the data is only as current as its sources.

A point-in-polygon join

Which places sit inside a building? It’s a join on ST_Intersects. DuckDB builds a spatial join plan automatically, so no index is needed:

con.sql("""
CREATE OR REPLACE TABLE place_building AS
SELECT p.id AS place_id, b.id AS building_id
FROM places p JOIN buildings b ON ST_Intersects(b.geometry, p.geometry)
""")
Output — places in buildings
point-in-polygon join of 20,727 places x 144,965 buildings: 0.05 s
places inside a building footprint: 10,147 (49.0%)
name subtype area_m2 places
The Galleries commercial 10267.0 62
NaN commercial 331.0 61
NaN commercial 9104.0 59
NaN residential 56.0 50
NaN residential 58.0 49
NaN residential 57.0 47

The join took 0.05 s, and only 49% of places fall inside a footprint. Most of the rest are on the pavement in front of the shop, because place points are often geocoded to the street. The table also shows why you should look at your data. Three small 56–58 m² houses each hold about 50 places. These are most likely businesses registered at a home address, or an accountant’s address used by many companies. Filter on confidence, or drop buildings with implausible counts, before you use these numbers.

Zoomed map of Broadmead and Cabot Circus. Buildings are shaded from yellow to red by the number of places inside them, with black dots for places inside a building and blue crosses for places outside every building, many of them lining the streets
Broadmead and Cabot Circus: 1,870 of 3,368 places fall inside a footprint. Many of the others (blue crosses) sit on the street outside the shop they describe.

Aggregate to H3 hexagons

Hexagons make a fair, regular grid for comparing density. The community h3 extension turns a point into a cell ID. We group buildings by the cell of their centroid and places by their own cell, join the two, and keep only hexagons that lie wholly inside the download box:

con.sql("""
CREATE OR REPLACE TABLE hex AS
WITH b AS (
SELECT h3_latlng_to_cell(ST_Y(ST_Centroid(geometry)), ST_X(ST_Centroid(geometry)), 9) AS h3,
count(*) AS buildings, sum(area_m2) AS footprint_m2,
count(*) FILTER (WHERE subtype = 'residential') AS residential
FROM buildings GROUP BY h3),
p AS (
SELECT h3_latlng_to_cell(ST_Y(geometry), ST_X(geometry), 9) AS h3,
count(*) AS places,
count(*) FILTER (WHERE category_group = 'food_and_drink') AS food_drink
FROM places GROUP BY h3)
SELECT h3_h3_to_string(h3) AS h3, coalesce(buildings, 0) AS buildings, coalesce(residential, 0) AS residential,
coalesce(places, 0) AS places, coalesce(food_drink, 0) AS food_drink,
round(100 * coalesce(footprint_m2, 0) / h3_cell_area(h3, 'm^2'), 1) AS built_pct,
ST_GeomFromText(h3_cell_to_boundary_wkt(h3)) AS geometry
FROM b FULL JOIN p USING (h3)
WHERE ST_Within(ST_GeomFromText(h3_cell_to_boundary_wkt(h3)), ST_MakeEnvelope(-2.66, 51.42, -2.52, 51.50))
""")
Output — H3 resolution 9
built 758 hexagons in 0.11 s
h3 buildings built_pct places food_drink
8919583922fffff 196 49.7 534 143
89195876c93ffff 179 39.3 413 86
8919583926fffff 199 39.4 285 62
r_built_places r_residential_share_food
0.58 -0.13

The top food-and-drink hexagon (143 places) covers the Old City, around Corn Street and St Nicholas Market. The second (86) is its neighbour to the west, around the Centre and Baldwin Street. The line of orange cells running north in the hexagon map below follows Stokes Croft and Gloucester Road, one of Bristol’s best-known independent high streets. Built cover and place counts are moderately correlated (r = 0.58). Food and drink is weakly negatively related to the residential share (r = −0.13).

Two hexagon maps of central Bristol. Left: building footprint cover per H3 cell, highest at about 50 to 55% in the old city centre and lowest in parks and along the river. Right: food and drink places per cell, with a dark hotspot in the centre and a line of orange cells running north along Stokes Croft and Gloucester Road
758 H3 resolution-9 hexagons (about 0.1 km² each). Left: building footprint cover. Right: food and drink places, with the hotspot in the old city and the Gloucester Road corridor running north.

Step 5: from DuckDB to GeoPandas

To hand a DuckDB table to GeoPandas, export the geometry as WKB and decode it with Shapely. It’s quick: all four tables took 0.23 s.

import geopandas as gpd
import shapely
def to_gdf(table):
df = con.sql(f"SELECT * EXCLUDE geometry, ST_AsWKB(geometry) AS wkb FROM {table}").df()
geom = shapely.from_wkb(df.pop("wkb").map(bytes))
return gpd.GeoDataFrame(df, geometry=geom, crs="OGC:CRS84")
buildings = to_gdf("buildings")

You can also read DuckDB’s Parquet output directly with gpd.read_parquet(), because DuckDB writes GeoParquet metadata whenever a table has a geometry column. Watch the metadata, though.

Step 6: write GeoParquet with the right metadata

GeoPandas writes GeoParquet 1.1 when you ask for it. With write_covering_bbox=True it adds a bbox struct column, as Overture does, so other readers can filter by area without decoding geometry:

buildings.to_parquet("output/bristol_buildings.parquet", compression="zstd",
schema_version="1.1.0", write_covering_bbox=True)
Output — GeoParquet metadata
output/bristol_buildings.parquet 144,965 rows 18.86 MB
output/bristol_places.parquet 20,727 rows 1.75 MB
output/bristol_water.parquet 161 rows 0.14 MB
output/bristol_hex.parquet 758 rows 0.08 MB
'geo' metadata (GeoPandas): {"version": "1.1.0", "primary_column": "geometry", "encoding": "WKB", "geometry_types": ["MultiPolygon", "Polygon"], "bbox": [-2.6606, 51.4195, -2.5192, 51.5004], "covering": {"bbox": {"xmin": ["bbox", "xmin"], "ymin": ["bbox", "ymin"], "xmax": ["bbox", "xmax"], "ymax": ["bbox", "ymax"]}}, "crs": "PROJJSON OGC:CRS84"}
'geo' metadata (DuckDB): {"version": "1.0.0", "primary_column": "geometry"} | keys of the geometry column: ['bbox', 'encoding', 'geometry_types']

The difference matters. GeoPandas records the CRS as PROJJSON, the geometry types, the dataset bbox and the covering column. DuckDB 1.5.6 wrote GeoParquet 1.0.0 metadata with no crs key. The specification says a missing CRS means OGC:CRS84, so readers assume longitude/latitude. That is correct here, but it would be silently wrong if you had transformed the geometry to EPSG:27700 in SQL. If your data isn’t in longitude and latitude, write the final file with GeoPandas (or set the CRS when you read it back).

These four files in output/ are the input to part 2.

Step 7: GeoParquet vs Shapefile, GeoPackage and FlatGeobuf

How does GeoParquet compare with the classic formats on real data? The script writes the same 144,965 buildings with 8 attribute columns to five formats and times a write, a full read and a read of a 1 km box near Temple Meads (846 buildings). Each time is the median of three runs on a laptop SSD:

Output — format benchmark
format size_MB write_s read_s bbox_read_s bbox_rows
GeoParquet (DuckDB) 13.5 0.14 0.13 0.053 846
GeoParquet (GeoPandas) 18.8 0.35 0.15 0.044 846
FlatGeobuf 52.4 1.76 0.73 0.009 846
GeoPackage 48.8 1.38 0.25 0.011 846
Shapefile 193.2 2.36 0.98 0.580 846
Four bar charts comparing five formats for 144,965 building polygons: file size, write time, full read time and 1 km bbox read time. GeoParquet is smallest and fastest to write and read in full; FlatGeobuf and GeoPackage are fastest for the small bbox read; Shapefile is largest and slowest in every test
The same 144,965 buildings in five formats (median of 3 runs). GeoParquet wins on size and full reads; the formats with a spatial index win small-area reads.

The results:

  • Size: GeoParquet is 3.6–3.9× smaller than GeoPackage or FlatGeobuf, thanks to columnar zstd compression. DuckDB’s file is smaller than GeoPandas’s because it has no bbox covering column. The Shapefile is 193 MB, 14× the GeoParquet. Its DBF stores every text field at a fixed width of up to 254 characters, and Overture’s 36-character IDs fill a lot of that.
  • Full reads and writes: GeoParquet read the whole file 2–8× faster and wrote it 4–17× faster than the GDAL formats.
  • Small-area reads: FlatGeobuf (0.009 s) and GeoPackage (0.011 s) are 4–5× faster than GeoParquet, because they have a real spatial index. GeoParquet relies on row-group statistics, which help most with large, spatially sorted files like Overture’s. The Shapefile had no .qix index and took 0.58 s.

So use GeoParquet to store, share and analyse data in bulk, and GeoPackage or FlatGeobuf when an application reads small windows over and over. Fast vector geoprocessing in Python runs a similar benchmark on 200,000 points, with spatial indexes in GeoPandas.

Licences and attribution

Overture data is open, but each theme has its own licence, and the places theme varies record by record:

Theme Licence What it means for you
Buildings ODbL 1.0 (OpenStreetMap, Microsoft ML Buildings, Esri Community Maps, Google Open Buildings and others) Attribute; a shared derived database must also be ODbL
Base (water, land use, land) ODbL 1.0 As above
Transportation, divisions ODbL 1.0 As above
Places CDLA-Permissive-2.0 (Meta, Microsoft and others), Apache-2.0 (Foursquare), CC0-1.0 (AllThePlaces) Permissive; keep the licence text or notice
Addresses Varies by source Check the sources column

For maps and figures, Overture recommends the line “© OpenStreetMap contributors, Overture Maps Foundation”. Add other sources where their licences ask for it (Microsoft, Esri and Google for buildings). The figures on this page carry that credit. Because our bristol_buildings.parquet is a derived database of ODbL data, share it under the ODbL too. Check the Overture attribution page for each release: sources are added often.

Limitations and caveats

  • Coverage is uneven. Heights are missing for 86% of Bristol’s buildings, 30.5% have no subtype, and 1,338 places have no category group. Overture is a merge of sources, not a survey.
  • Places are noisy. Duplicate and closed businesses remain, and some points sit at registered addresses rather than shopfronts. Filter on confidence, and use operating_status where the source fills it.
  • Releases change. Overture publishes monthly and keeps only recent releases in the bucket. Old paths disappear, which is why the script reads latest from STAC. Feature IDs are designed to be stable across releases, but they can still change.
  • Timings depend on your network. The remote queries ran from the UK to AWS us-west-2. Expect different absolute numbers elsewhere. The local benchmark depends on your disk and CPU.
  • The box clips features. Buildings are selected when their bbox overlaps the area, so some hang over the edge. Water polygons are clipped, and edge hexagons are dropped.

Frequently asked questions

How do I download Overture Maps data for one city in Python?

Use DuckDB with the httpfs and spatial extensions. Read s3://overturemaps-us-west-2/release/<release>/theme=<theme>/type=<type>/*.parquet with a WHERE filter on the bbox struct column, and COPY the result to a local Parquet file. Overture’s STAC collections.parquet index tells you which files overlap your box, which made the query 15× faster here. The overturemaps Python CLI (overturemaps download --bbox=… -f geoparquet --type=building) wraps the same idea if you just want a file.

Is GeoParquet better than Shapefile or GeoPackage?

For storing and analysing data, usually yes. Here GeoParquet was 3.6× smaller than GeoPackage and 14× smaller than a Shapefile, and 2–8× faster to read in full. It has no field-name limits or 2 GB cap, and it stores the CRS as PROJJSON. GeoPackage and FlatGeobuf still win for repeated reads of small areas, because they have a spatial index, and for editing in desktop GIS.

What is the latest Overture Maps release?

On 19 October 2026 it was 2026-09-23.1 (schema 2.0.0). Overture releases monthly. Read the latest field from https://stac.overturemaps.org/catalog.json, as the script does, rather than hard-coding a date: old releases are removed from the bucket after a few months.

Can I use Overture Maps data commercially?

Yes. All themes are under open licences that allow commercial use, but the conditions differ. ODbL themes (buildings, base, transportation, divisions) need attribution, and derived databases you share must stay ODbL. Places are under permissive licences (CDLA-Permissive-2.0, Apache-2.0 or CC0) by source. Credit “© OpenStreetMap contributors, Overture Maps Foundation” on maps.

Do I need DuckDB, or can GeoPandas read Overture directly?

GeoPandas can read the remote files with gpd.read_parquet(url, bbox=…, filesystem=…) and pyarrow’s S3 support, but it can’t run SQL. You would have to flatten the nested names, sources and taxonomy columns in pandas. DuckDB filters, projects and flattens before anything reaches Python. The usual pattern is DuckDB for extraction and heavy SQL, then GeoPandas for plotting and the Python ecosystem.

Why does DuckDB say my GeoParquet file has no CRS?

DuckDB 1.5.6 writes GeoParquet 1.0 metadata without a crs key, which by the specification means OGC:CRS84 (longitude/latitude). That is right for Overture data. If you transform to a projected CRS in SQL, write the final file with GeoPandas (to_parquet(schema_version="1.1.0")) so the CRS is recorded.

Next steps

Data and licences

  • Overture Maps Foundation, release 2026-09-23.1, accessed from s3://overturemaps-us-west-2 on 19 October 2026. Buildings and water: ODbL 1.0, © OpenStreetMap contributors, Microsoft ML Buildings. Places: CDLA-Permissive-2.0 (Meta, Microsoft, DAC, PinMeTo), Apache-2.0 (Foursquare) and CC0-1.0 (AllThePlaces). See the Overture attribution guide.
  • H3 hexagonal grid: Uber Technologies, Apache-2.0. DuckDB h3 community extension by Isaac Brodsky.
  • Code: overture_duckdb.py, tested with Python 3.12.0, DuckDB 1.5.6, GeoPandas 1.2.0, pyogrio 0.13.0 (GDAL 3.12.4), pyarrow 26.0.0 and Shapely 2.2.0.

Key terms

Overture Maps
An open map dataset from the Overture Maps Foundation (Amazon, Meta, Microsoft, TomTom and others) that merges OpenStreetMap with other sources into themes such as buildings, places, transportation and base, released monthly as GeoParquet.
GeoParquet
An open specification for storing vector geometries in Apache Parquet files: WKB or native geometry columns plus a 'geo' metadata block with the CRS, geometry types and bounding box.
DuckDB spatial
An extension for the in-process DuckDB database that adds a GEOMETRY type and ST_ functions (ST_Intersects, ST_Area, ST_Transform and more), similar to PostGIS but with no server.
Row group
A horizontal chunk of a Parquet file with its own column statistics. Readers skip row groups whose min/max values cannot match a filter, which is what makes bbox queries over HTTP fast.
H3
Uber's hierarchical hexagonal grid. Resolution 9 cells are about 0.1 km², a handy size for city-scale summaries.
Bbox covering
A GeoParquet 1.1 feature: a struct column (xmin, ymin, xmax, ymax) for each row, so readers can filter by area using ordinary Parquet statistics.
MapLibre GL JS map of central Bristol at zoom 15.6, with Overture building footprints coloured by subtype and places as coloured dots by category, around Broadmead and Cabot Circus

Tutorial Python Part 2 of 2

PMTiles vector tiles in Python and a MapLibre GL JS web map

Turn Overture Maps GeoParquet into PMTiles vector tiles with the GDAL that ships in pyogrio, no Tippecanoe needed, measure the zoom-level and size trade-offs, and serve an interactive MapLibre GL JS map of Bristol from static hosting.

Intermediate 12 min read

Tutorial Python

Hot spot analysis in Python: Getis-Ord Gi*, Moran's I and LISA on Birmingham road collisions

Run a hot spot analysis in Python with PySAL: aggregate 2025 STATS19 road collisions in Birmingham to a hexagon grid, test for clustering with global Moran's I, map Getis-Ord Gi* hot spots and LISA clusters with permutation inference and FDR correction, and see how spatial weights, the areal unit and exposure change the answer.

Intermediate 19 min read