Reducing Coordinate Precision Before Archival

Rounding coordinates is a one-line operation and a three-step procedure: choose the grid from the source accuracy, snap in a way that repairs the topology snapping breaks, and prove nothing moved further than intended. This walkthrough covers all three for a production vector archive, including the case that trips up naive implementations — adjacent polygons whose shared boundary separates when each is rounded independently.

Choosing the Grid from the Source, Not from Habit

The grid size is derived, not picked. It comes from the positional accuracy recorded in the survey metadata, and where several sources are merged into one archive, from the least accurate of them.

Deriving the rounding grid from stated source accuracy Four collections with their stated positional accuracy and the rounding grid derived from it, each grid at or just below the accuracy figure. collectionstated accuracygridgeometry column saving cadastral, total station ±15 mm10 mm18% utility network, GNSS ±80 mm50 mm29% aerial photogrammetry ±250 mm100 mm37% digitised historic mapping ±2.5 m1 m54%

Step-by-Step Procedure

Step 1 — Confirm the reference system and read the accuracy

# The grid is in the units of the storage CRS: metres here, not degrees
python3 - <<'PY'
import geopandas as gpd, json
gdf = gpd.read_parquet("staging/utilities_2026.parquet")
meta = json.load(open("staging/utilities_2026.meta.json"))
print("crs:", gdf.crs.to_epsg(), "units:", gdf.crs.axis_info[0].unit_name)
print("stated accuracy:", meta["positional_accuracy_m"], "m")
PY
# crs: 27700 units: metre
# stated accuracy: 0.08 m

Rounding a geographic coordinate needs a grid in degrees, which varies in ground distance with latitude — one more reason archives that round usually store projected coordinates.

Step 2 — Snap with topology repair, not with a numeric round

import geopandas as gpd, numpy as np
from shapely import set_precision, get_coordinates

GRID = 0.05  # metres, from the stated accuracy above

gdf = gpd.read_parquet("staging/utilities_2026.parquet")
before_coords = get_coordinates(gdf.geometry.values)
before_n = len(gdf)

# set_precision snaps to the grid AND repairs the topology snapping can break
gdf["geometry"] = set_precision(gdf.geometry.values, grid_size=GRID)

# Features that collapsed entirely become empty rather than disappearing silently
collapsed = gdf.geometry.is_empty
print(f"collapsed features: {collapsed.sum()}")
gdf = gdf.loc[~collapsed]

np.round on raw coordinate arrays is the naive alternative and produces self-intersections, zero-length segments and duplicate consecutive vertices — all of which are valid float values and invalid geometry.

Step 3 — Deduplicate consecutive vertices

Snapping frequently makes neighbouring vertices identical. They are redundant, they inflate the column, and some readers treat them as degenerate segments.

from shapely import remove_repeated_points
gdf["geometry"] = remove_repeated_points(gdf.geometry.values, tolerance=0)

Step 4 — Write, with the precision recorded

gdf.to_parquet("s3://spatial-archive/archive/utilities/2026/utilities.parquet",
               compression="zstd", compression_level=12,
               row_group_size=500_000, write_statistics=True)

import json, boto3
boto3.client("s3").put_object(
    Bucket="spatial-archive",
    Key="archive/utilities/2026/utilities.manifest.json",
    Body=json.dumps({"coordinate_grid_m": GRID, "storage_crs": "EPSG:27700",
                     "source_accuracy_m": 0.08, "collapsed_features": int(collapsed.sum()),
                     "tool": "shapely.set_precision", "written": "2026-08-11"}).encode())
Naive rounding versus precision snapping on a shared boundary Two adjacent polygons whose shared edge stays shared under precision snapping but separates into a sliver gap and an overlap under naive coordinate rounding. Two parcels sharing a boundary source edge shared exactly naive round sliver gap and overlap appear both polygons still “valid” precision snap both snap to the same grid nodes shared edge preserved

Validation & Verification

after = gpd.read_parquet("s3://spatial-archive/archive/utilities/2026/utilities.parquet")
after_coords = get_coordinates(after.geometry.values)

# Vertex counts differ after deduplication, so compare bounds rather than positions element-wise
shift = np.abs(np.array(gdf.total_bounds) - np.array(after.total_bounds)).max()
print(f"bounds shift {shift:.4f} m · invalid {(~after.geometry.is_valid).sum()} "
      f"· features {before_n} -> {len(after)}")

Expected output is a bounds shift at or below half the grid size, zero invalid geometries, and a feature count reduced only by the collapsed count already reported. A shift larger than half the grid means the rounding happened in the wrong reference system.

Troubleshooting

Symptom Root cause Fix
Invalid geometries after rounding np.round used instead of a precision model Use set_precision, which repairs as it snaps
Gaps between adjacent polygons Features snapped independently in separate jobs Snap the whole layer in one operation with one grid
Feature count drops unexpectedly Slivers narrower than the grid collapsed Expected — report the count; reduce the grid if the slivers are real
No size saving after rounding Coordinates re-expanded by a later reprojection Round last, after the storage reference system is final
Saving smaller than the table predicts Geometry column was already low precision Measure before and after rather than assuming the default was float64

Operational Execution Checklist

What Rounding Does to File Size, Measured

The saving from rounding is real and smaller than the digit count suggests, because the codec was already finding some of that redundancy. Measuring both the raw and the compressed change is what keeps the expectation honest.

Measured effect of rounding on a 48-million-vertex layer Four rounding grids with the resulting raw and compressed geometry-column sizes, showing that compression absorbs part of the nominal saving. grid raw column compressed column none (float64) 768 MB 296 MB 1 mm 768 MB 241 MB — −19% 1 cm 768 MB 198 MB — −33% 10 cm 768 MB 164 MB — −45% Raw size never changes: rounding a float leaves a float. The whole saving is compressibility, which is why it must be measured after the codec rather than before.

The raw column is identical in every row because rounding does not change the storage type — the mantissa is simply full of zeros. That is also the argument for pairing rounding with an integer encoding, which converts the same information into fewer bytes before the codec sees it.

Frequently Asked Questions

Should rounding be applied to the archive or to a derived copy?

To the archive, if the discarded digits were never measurements — that is the entire argument. Rounding only a derived copy leaves the archive paying for noise indefinitely, and since the archive is where the retention window applies, that is where the saving compounds. Keep the unrounded source until the rounded archive is validated.

What grid suits an archive merging several sources?

The one derived from the least accurate source in the merged layer, since a shared grid is what keeps topology consistent across the join. Where the accuracy difference is large — centimetre survey merged with metre-scale digitised mapping — keeping them as separate layers with their own grids is usually better than degrading the accurate one.

Does rounding affect spatial indexes or statistics?

Both are recomputed from the rounded geometry when the file is written, so they stay consistent automatically. What does need attention is any external index built against the unrounded data, which will be stale by up to half the grid size — small, but enough to matter for exact-boundary queries.

Does rounding need to be applied to every geometry column?

To every column whose coordinates are stored, including any simplified display geometry, and to the bounding-box covering columns derived from them. A rounded geometry beside an unrounded bbox produces a covering that is fractionally wrong, which is harmless for pruning and confusing for anyone comparing the two.

How does rounding interact with topology validation?

It creates work for it, which is why the two belong in the same step. Snapping can produce self-intersections, zero-length segments and collapsed rings, and a precision model that repairs as it snaps handles most of them. Running a validity check afterwards catches the rest, and the count of repairs is worth recording — a layer needing thousands of repairs was probably rounded too aggressively.

Is there a case for rounding differently within one dataset?

Rarely, and it complicates everything downstream. Where a dataset genuinely mixes accuracies — surveyed boundaries alongside digitised approximations — the cleaner arrangement is separate layers with separate grids and an accuracy attribute, rather than one layer whose precision varies invisibly from feature to feature.

Does the rounding step belong in the conversion job or in a separate pass?

In the conversion job, immediately before the write. A separate pass means reading and rewriting the data an extra time, and it creates a window in which an unrounded version exists and may be published by mistake. Folding it into the job that produces the archive object is both cheaper and safer.

How is the decision reviewed later?

Through the manifest, which records the grid, the source accuracy and the reference system it was applied in. A reviewer comparing those three can tell immediately whether the choice was defensible, without re-running any measurement. That is the practical test of whether the record is adequate: someone who was not there should be able to judge the decision from it.

What should be done when the source accuracy varies within one dataset?

Round to the least accurate component, or split the dataset. A layer merging surveyed boundaries with digitised approximations has no single defensible grid, and choosing the finer one archives noise for most of the features while choosing the coarser one degrades the good ones. Splitting by accuracy class, with the class recorded as an attribute, is more work at ingest and far more honest thereafter.

Does the rounding need to be applied to Z values as well?

Yes, and usually with a different grid. Vertical accuracy is typically two to three times worse than horizontal for the same survey, so a common horizontal grid applied to elevations stores digits the instrument never resolved. Setting the two independently is a small change to the write and a measurable saving on three-dimensional data.

Up one level: Coordinate Precision & Encoding Strategies.