Skip to content
CityParquet is v0.1.0-draft — the specification is still changing.
Esc
↑↓navigate↵open⌘Jpreview

Format specification · v0.1.0-draft

CityParquet

A columnar encoding of the CityGML data model

no database

Run your first queryTry it in your browserread_parquet('city/building.parquet')
StorageStatic filesNo database server
LayoutOne object per rowNo joins on read
InteroperabilityParquet · WKB · SQLGeoParquet at LoD0
MetadataSTAC ItemCatalogue-ready
01

The problem

Today's workflow

Before you can ask a question, you have to download, parse, or load the city model into a database.

Download first

CityGML and CityJSON are text. A query normally begins by pulling gigabytes of a single region to a local machine.

Parse everything

Even locally, every query parses the whole document into memory before computing anything. Slow and memory-hungry.

Or run a database

3DCityDB works, but you host a server and load the data — and hit cost and scale limits at national size.

Our aim: national-scale 3D analysis in seconds, rather than hours.

Read only the data a question needs, directly from static files — using familiar tools, without running a database server.

02

How it works

Read path

Read the columns you need. Skip places you do not. Fetch only the matching parts of the file.

Columnar encoding

Attributes and levels of detail live in separate columns. A query about building height can read that column without decoding every building's geometry or parsing a whole text file.

Spatial filtering without a server

Nearby objects are stored together. Bounding-box statistics let a reader skip groups outside the query area — an index-like benefit stored in the file itself, without a database service.

Partial retrieval over HTTP

HTTP range requests fetch the required byte ranges from object storage. You can query a published package without downloading the entire dataset first.

The result retains the CityGML semantics: object types, relationships, levels of detail, and semantic surfaces travel with the geometry.

Where the model lies object storage · tens of gigabytes What the query touches column projection + row-group statistics What comes back still a semantic 3D model · megabytes s3://city-models/3dbag/ a bucket · not a host building.parquet transportation.parquet vegetation.parquet metadata.json one directory, split by CityGML module one row per city object · nothing running in front of it id object_type height geometry_lod0_0 geometry_lod1_0 geometry_lod2_2 geometry_properties_lod2_2 building.parquet column projection — only these columns are fetched row-group statistics — only this band is fetched GET Range: bytes=… no server roof wall ground semantic surfaces survive the read
03

Where it sits in the family

Encodings

One conceptual model, several encodings. Each writes the same semantics — a wall is a WallSurface — for a different job. CityParquet is the columnar member and replaces none of the others. Compare it with each of them.

CityGMLGML / XML
CityJSONJSON text
3DCityDBRelational
FlatCityBufRow binary
CityParquetColumnar

Use CityParquet when the job is analysis

Filtering, aggregating, or measuring over a large collection in object storage. The query reads the bytes it needs, not the file.

Use CityJSON when the job is exchange

Handing data to another tool or person, or editing it. CityParquet round-trips back, so deliver in one and exchange in the other.

04

Measured behaviour

Benchmark

File sizes on disk for Montréal, 31,415 CityObjects, the largest dataset in the benchmark corpus, using Hilbert-ordered CityParquet.

95%smaller than CityGML159 MB versus 3.26 GB
68%smaller than CityJSON159 MB versus 498 MB
57%smaller than FlatCityBuf159 MB versus 366 MB

Projection pushdown, per LoD

A model carries one geometry column per LoD. A query ongeometry_lod2_2 never reads the LoD 1.2 bytes.

Spatial row-group pruning

The bbox column feeds per-row-group statistics; Hilbert ordering clusters near rows so more groups can be skipped.

CityParquet combines compact storage with fast selective reads. FlatCityBuf leads the measured spatial and full-read scenarios; CityParquet does not win every workload.

05

Tooling

Ecosystem

SQL on a 3D city model, with nothing in between.

A package is Parquet on object storage, so DuckDB opens it as it stands — read_parquet over HTTPS, no import step, no server, no conversion. Two community extensions carry the parts Parquet has no vocabulary for: cityjson reads the text formats, three_d turns stored WKB back into solids you can measure. The example below uses a stored volume attribute, so it needs no geometry extension.

cityparquet-rs

Reference implementation and CLI: convert, export, compare, bench.

cityjson/cityparquet
duckdb-cityjson

Community extension reading CityJSON and CityJSONSeq into the wide layout.

cityjson/duckdb-cityjson
duckdb-3d

Measurable solids from stored WKB: volume, height, validation reports.

cityjson/duckdb-3d
Arrow · pandas · Spark

A package is Parquet, so mature readers open it with no extension at all.

One region, out of a whole country

“Give me the total building volume in this area of Delft, out of roughly 11 million buildings across the Netherlands.”

This example selects buildings whose bounding boxes overlap a 1 km × 1 km area, then adds their recorded LoD 2.2 volumes. The coordinates define the area in the dataset’s Dutch national grid. DuckDB can skip unrelated row groups and read just the columns needed.

Total building volume in a selected Delft area
SELECT ROUND(SUM(b3_volume_lod22)) AS total_volume_m3
FROM read_parquet(
  'https://cityparquet.open3d.city/data/3dbag/building.parquet')
WHERE object_type = 'Building'
  AND bbox.xmin < 85000 AND bbox.xmax > 84000
  AND bbox.ymin < 447000 AND bbox.ymax > 446000;
Example query · stored LoD 2.2 volume in cubic metres · not run live

DuckDB is a trademark of the DuckDB Foundation. CityParquet is an independent project and is not affiliated with or endorsed by it.

06

AI friendly

Agents

Ask a question. Let your agent query the city model.

Scripted conversations · not a live chat
You

How many buildings in the Netherlands are higher than 100 metres?

AI agent
Calling the CityParquet query tool…Running SQL against the national 3DBAG dataset…
View SQL
SELECT COUNT(*) AS buildings_over_100m
FROM read_parquet(
  'https://cityparquet.open3d.city/data/3dbag/building.parquet')
WHERE object_type = 'Building'
  AND b3_h_dak_max - b3_h_maaiveld > 100;

674 buildings are higher than 100 metres in this dataset.

Using roof height above ground. Only building type and height attributes are needed — no 3D geometry to download.

Try the query →

Query result checked on 23 September 2026.

Five minutes to the first query

Install DuckDB, load two extensions, point a query at a package — or skip all three and query 3DBAG in the browser playground. The draft specification is in the docs.