geomermaids · GeoPQ Workbench

Open, inspect, fix and query GeoParquet. A fast native desktop app: no server, no GDAL, no accounts. Free and open source.

Download: prebuilt binaries for macOS, Windows and Linux — the macOS DMGs are signed and notarized, so it is drag-to-Applications and go. Source and issues on GitHub (MIT / Apache-2.0).

Two minutes, one real dataset: the 3.5 GB MassGIS statewide parcels — two dropped shapefiles imported and merged, optimized into a single 536 MB GeoParquet file, published to S3 and reopened in place.

Why it exists

Most GeoParquet in the wild is a raw database export that performs far below what the format allows: no spatial ordering, no bbox covering column, oversized row groups. The file opens, but nothing can be read incrementally, so every viewer chokes on it.

GeoPQ Workbench is opinionated about this on purpose. Every file you open is graded against the levers that actually matter, the scorecard explains each miss in plain language, and one click rewrites the file into a best-practice one. It doubles as a hands-on guide to the format: the same material as my GeoParquet writing cookbook, but live, against your own data.

What it does

Renders at native speed

Pure Rust with GPU pipelines (egui + wgpu): 3.75M points load in ~180 ms and pan/zoom at vsync. Memory scales with what is on screen, not with file size.

Opens everything GeoParquet

1.0, 1.1 (WKB and GeoArrow), 2.0 native GEOMETRY/GEOGRAPHY types, untagged parquet with a guessable geometry column, even bare lon/lat tables.

A CSV or parquet with coordinate columns becomes a real file rather than a viewer trick: pick X and Y and it is written out as GeoParquet 1.1 and opened as a layer, attributes and all. Names find the columns, the values decide the CRS. Degrees fit inside 180 and 90 and a projected grid does not, so a national grid in metres asks for its EPSG code instead of being guessed as 4326 and landing a continent away.

Datasets too heavy to draw

CORINE Land Cover is 2.4M polygons and 7.7 GB of geometry, with single features carrying more than 200,000 vertices. Where a file offers a bbox covering column, every feature is drawn from its box instead of its geometry: all of Europe in about a second and a gigabyte, not a sample of it. Zoom past the point where a feature is more than a few pixels wide and the real geometry loads for what you are looking at. The switch is by scale, so the same zoom behaves the same over a city and over open country.

Published palettes

A dataset whose classes have an official palette arrives drawn in it. CORINE is recognized from its class column and rendered from the published table, 111 is continuous urban fabric at RGB 230,0,77, with nothing read from the data. QGIS colour-map exports load too, so a dataset that ships one is styled by it.

Grid summaries

Aggregate any numeric column onto square, H3 or A5 cells, with areal apportionment so a feature spanning several cells gives each one the share it actually covers. Mean, median, sum, count or density, smoothing, focal standard deviation, morphological open and close, hillshade, and contour lines at value quantiles. 2.56M parcels to a 1 km grid in ~0.7 s.

Styling that explains itself

Seven classification methods including Jenks and head/tail breaks, an interactive legend where a click hides a class and ↻ refits the classes to what is currently in the viewport, and normalize-by-area measured on an equal-area projection rather than in degrees².

Point layers carry a symbol: circle, square, triangle, diamond, hexagon or star, each drawn as a distance field on the GPU and sized to the area of the circle it replaces, so one radius slider keeps its meaning and a layer keeps its ink weight when the symbol changes. The border takes a colour of its own, or none.

Lines and outlines take a dash pattern — dash, long dash, dot, dash-dot — with round, square or flat caps, the pattern following the line's own arc length so it runs through vertices instead of restarting at every segment. And the classification can drive stroke width across its classes, a min-to-max ramp beside the colour ramp.

Credits where they are due

A layer that carries an attribution shows it in the map corner and in full under File info, read from the parquet metadata or from an ATTRIBUTION.txt beside the data, searched upwards from the file's own folder so one notice at a repository root credits everything under it. A STAC collection carries neither, so the credit is built from what the catalog declares: Overture's buildings are ODbL and name OpenStreetMap's contributors, its bathymetry is CC0 and does not, because saying otherwise would be a false claim about where the data came from.

Basemaps that follow the projection

Tiles are published in Web Mercator, so in any other projection each one is drawn as a mesh with its vertices projected exactly: no seams between tiles, and coastlines that still line up. Place names are pixels, though, and pixels shear, so the view is measured and a labelled style steps aside for its label-free twin once a name would visibly tilt. Carto, OpenStreetMap, OpenTopoMap and Esri imagery, with a Natural Earth coastline for the world views where the Mercator pyramid stops at ±85°. The map is framed from the file's metadata before any geometry is decoded, so the basemap downloads alongside the data instead of after it.

It is the bottom layer of the map, so it sits at the bottom of the layers panel rather than in a menu, with its own source list and an opacity slider, so fading it back keeps the context without it competing with the data on top. Each pass replaces what the fetch queue wants instead of appending to it, so zooming from z8 to z14 spends its requests on the view you are in, not on the five you left behind.

Quality scorecard

Seven checks against the footer alone: spatial index, spatial ordering, row groups, encoding, page index, compression, metadata. Each line explains itself.

One-click Optimize

Hilbert sort, tuned row groups, bbox covering, zstd, page indexes — output as 1.1 WKB, 1.1 GeoArrow or 2.0 native, with optional H3 or admin columns and hive/H3 partitioned output.

It streams. The rewrite is two passes over the source — keys first, then a gather through a byte-budgeted cache — so memory follows the row count, not the file size, and the old 8 GB ceiling is gone. Outputs are crash-safe: everything builds under a .partial name and is published by a single rename, so an interrupted export can no longer leave a truncated parquet that looks real, and a partitioned dataset appears whole or not at all.

Remote-native

Open https:// and s3:// files in place over range requests: a 304 MB file opens in ~1.3 s, and only the row groups under your viewport get downloaded. A hive-partitioned s3://bucket/prefix/ or state=* glob opens as one layer, partitions loading in parallel — and an https:// prefix opens the same way through the STAC collection.json published at it, hive path segments becoming columns, parts pruned by their bboxes. The status bar shows live bandwidth, layer data and basemap tiles counted apart, and File info reports what each layer has cost in bytes and in range requests. Overture Maps and CORINE Land Cover are preconfigured in the catalog browser.

A STAC collection opens the parts covering most of the current view and adds the others as you pan into them, most-overlapping first. Overture's buildings are 512 parts; a European view opens 16 of them and 80M rows, and panning to California finds the rest. Fragments are numbered in global order, so appending them leaves every row-group index meaning what it meant before: the layer keeps the geometry it already built, with no rebuild and no flicker.

Open-data catalogs

Nearly every municipal portal in North America — ArcGIS Hub, Socrata, CKAN — publishes a DCAT catalog at /data.json, and File → Data catalogs turns any of them into a browsable, searchable dataset list. Seventeen major US city portals come built in, New York to Sacramento, each verified to serve datasets the app opens; paste any other portal's URL and it joins them, for the session or saved for good. A geo-formats switch hides the CSV-only entries when only mappable data matters.

Each dataset takes its cheapest correct road: GeoParquet range-reads in place, CSV goes through the attribute-table import, GeoPackage and GeoJSON download and convert like a dropped file — with the publisher and licence the catalog states written beside the import, so the layer stays credited months later. Real portals are messy and the browser expects it: HTML disclaimers are not licences, a geodatabase wearing a GeoPackage media type is refused, and a feed the portal cuts off mid-stream is salvaged to the last complete dataset, labelled as cut short rather than passed off as whole.

SQL console

DataFusion with 28 ST_* spatial functions (including st_transform) over every loaded layer, local or remote, with spatial predicate pushdown into the parquet reads. Results highlight on the map or export as new layers.

Polygon set operations (st_union, st_intersection, st_difference, st_symdifference) and spatial aggregates: st_union_agg dissolves a group, st_extent bounds it, st_collect gathers it without merging. Autocomplete resolves table aliases, so after from communes c typing c. offers that table's columns and nothing else.

Attribute tables and joins

A parquet or CSV with no geometry opens for its columns alone, then joins onto a layer on a shared key. The join builder reports the match count before you commit, because a join on a mismatched key matches nothing and the only other symptom is an empty layer.

Import is a dialog, not a guess: per column the sampled values, the proposed type and what forcing another one would cost. One NA in ten thousand rows turns a column of counts into text, and INSEE codes read as integers until the first Corsican 2A004 arrives. Delimiters are detected, because most European public data is semicolon delimited. Number format is a choice rather than a switch, because 3,739 is three thousand in one convention and three point seven in the other, and nothing in the value says which.

Column selection is pushed into the parquet scan, so a 532 MB census extract with 88 columns imports three of them in 38 seconds and 19.7M rows, or refuses in 200 ms with a sentence about narrowing it, rather than downloading half a gigabyte to say the same thing. The column list itself comes from the footer and is on screen in under a second, with sample values filling in behind it.

Import & merge

Pure-Rust importers for GeoPackage, Shapefile and GeoJSON — drag & drop, ESRI-flavored .prj and codepage sidecars handled. No confirmation step: conversion starts the moment the file lands, the dialog only appears to pick a table when a GeoPackage has several, and a file already converted reopens from its parquet at no cost. Compatible layers merge into one file on export, reprojected onto a common CRS.

Publish to S3 / R2

The Export dialog uploads the optimized file (or a hive-partitioned tree) straight to a bucket, multipart, using your AWS profiles — then anyone can open it in place by URL. Already-optimized layers publish as-is.

A STAC collection.json goes up beside the data — extent and row counts read from the parquet footers, one asset per part with its own bbox — as the distributing-GeoParquet best practices recommend. That document is also what makes the dataset reopenable: paste its https:// prefix into any copy of the workbench and the parts open as one layer, because the collection is the directory listing plain HTTPS never had.

Figures, not screenshots

File → Export map view (SVG) writes what is on screen as real vector paths: fills composited once per layer so overlaps don't darken, strokes with their dash pattern, caps and class widths, point markers in their symbol, graticule, coastline and layer credits included. It is the figure for the report, editable in Inkscape or Illustrator — save as .svgz and it ships gzip-compressed.

There are no barriers to entry either: a built-in catalog browser if you have no GeoParquet at hand, and no GDAL anywhere in the stack. An in-app cookbook covers the format's versions, encodings and performance levers.

One hard dataset, start to finish: CORINE Land Cover 2018 opened straight from the catalog over HTTP, 2.4M polygons and 7.7 GB of geometry. Every feature drawn from its covering box in the official palette, zoomed until the real geometry loads, then exported to a single file with only what the viewport holds. About three and a half minutes of real time, accelerated here through the download and export steps.

Try it in two minutes

  1. Download your platform's build from the releases page and launch it. Nothing to configure.
  2. Load data. Drag & drop a .parquet file, or File → Repositories… to browse Overture Maps and other public catalogs, or point it at a URL — for instance any file from my daily OSM GeoParquet snapshots.
  3. Open the file info to see the scorecard, then hit Export on a raw file and watch the before/after report.

Links

Found a bug, or a file it grades unfairly? Open an issue — real-world files are exactly what it is built for. And if you want help getting your organization's GeoParquet pipeline into shape, get in touch.