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.
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())
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.
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.
Related
- Coordinate Precision & Encoding Strategies — the parent topic covering representation as well as precision.
- Measuring Precision Loss Against Spatial Query Accuracy — proving a grid is safe before committing to it.
- Delta Encoding Vertex Coordinates in GeoParquet — the encoding that compounds the saving rounding produces.
- Automating CRS Transformations in ETL Pipelines — settling the reference system before rounding.
Up one level: Coordinate Precision & Encoding Strategies.