Compression Tuning & Storage Optimization for Geospatial Cold Storage

Petabyte-scale spatial archives only become affordable when compression, physical layout, and partitioning are tuned to the structure of the data rather than left at library defaults. This guide is the operational reference for data engineers, GIS archivists, cloud architects, and compliance teams who need to shrink cold-tier footprint and egress cost without sacrificing query performance, auditability, or retention guarantees. It connects entropy profiling, columnar layout, attribute encoding, and spatial partitioning into one production methodology you can enforce as code.

Optimization Pipeline at a Glance

Cold-storage optimization moves each dataset through profiling, compression, physical layout, and governed lifecycle transitions:

Cold-storage optimization pipeline Seven sequential stages — spatial dataset, profile entropy and cardinality, ZSTD level tuning, row group sizing, dictionary encoding, spatial partitioning, and cold-tier lifecycle — where each stage compounds the storage savings of the one before it, taking a typical archive from about three-fold to twelve-fold reduction. 1 Spatial dataset 2 Profile entropy & cardinality 3 ZSTD level tuning 4 Row group sizing 5 Dictionary encoding 6 Spatial partitioning 7 Cold-tier lifecycle Savings compound stage by stage ~3× → ~12× total reduction

Each stage compounds the savings of the one before it: profiling tells you how aggressively to compress, compression level interacts with row group size, row groups bound how well dictionaries pack, and partitioning determines how much of the archive a cold query has to touch at all. Treating these as a single tuning surface — rather than four independent settings — is what separates a 3x reduction from a 12x one.

Core Concepts & Definitions

The decisions throughout this guide depend on a shared vocabulary. These terms recur in every section below:

  • GeoParquet — a columnar storage format that encodes geometry (WKB) and attributes in separate, independently compressible columns, enabling predicate pushdown and selective decompression. It is the default cold-archive target produced by the GeoParquet Migration Workflows pipeline.
  • Row group — the atomic unit of read I/O inside a Parquet file. A scan never reads less than one row group’s worth of a column, so row group size sets the floor on time-to-first-byte and the granularity of statistics-based skipping.
  • ZSTD (Zstandard) — a tunable, dictionary-capable compression codec (levels 1–22) that dominates spatial archival because it pairs high ratios with fast decompression. Level selection is covered in depth under ZSTD Level Configuration for Spatial Files.
  • Dictionary encoding — a column encoding that replaces repeated values with small integer codes plus a lookup table, ideal for low-cardinality categorical GIS attributes.
  • Cardinality — the count of distinct values in a column; the primary signal for whether dictionary encoding helps or hurts.
  • Entropy — a measure of value unpredictability; high-entropy coordinate mantissas resist compression, while low-entropy categorical fields compress dramatically.
  • Spatial partitioning — splitting an archive into files keyed by a discrete spatial index (H3, S2, or Quadtree) so that bounded geographic queries prune the majority of objects before any byte is fetched.
  • Cold tier — an archival storage class (S3 Glacier Deep Archive, Azure Archive) with the lowest per-GB price but the highest retrieval latency and per-request cost, governed by the retention policy frameworks that lock objects for their mandated lifetime.
  • WORM — write-once-read-many object locking that makes archives immutable for a compliance window.

Cold Storage I/O Realities & Cost Drivers

Once spatial data crosses the cold threshold defined by your hot/warm/cold tier design, I/O patterns shift from random reads and frequent updates to sequential scans and targeted spatial predicates. Cloud object storage pricing models penalize inefficient retrieval through egress fees, per-object GET and restore request counts, and decompression compute overhead. Optimizing this transition requires a deliberate stack: modern columnar formats, algorithmic compression tuned to spatial entropy, and layout strategies that minimize data movement. Misaligned archives trigger unnecessary requests, inflate retrieval SLAs, and complicate compliance audits by scattering metadata across fragmented objects. Quantify each of these levers against real coefficients with the Spatial Archive Cost Modeling reference, which prices storage, retrieval, early-deletion penalties, and compression ratio as one model.

The cost model has four levers, and every section below moves at least one of them:

Lever What inflates it What this guide tunes
Per-GB storage Weak compression ratio ZSTD level + dictionary encoding
Restore / request count Too many small objects Row group + partition sizing
Egress volume Scanning more than the query needs Spatial partition pruning
Decompression compute Over-aggressive codec level Entropy-matched level selection
Cold-storage cost levers versus their controlling tuning knobs Each of the four cost drivers — per-GB storage, decompression compute, restore and request count, and egress volume — is paired with the single tuning knob that moves it. Trade-off brackets mark the two tensions: raising the ZSTD level shrinks storage while adding decompression CPU, and row-group and partition sizing balances request count against egress volume. COST LEVER TUNING KNOB THAT CONTROLS IT Per-GB storage inflated by a weak compression ratio Decompression compute inflated by an over-aggressive codec level Restore / request count inflated by too many small objects Egress volume inflated by scanning beyond the query ZSTD level + dictionary encoding Entropy-matched level selection Row-group + partition sizing Spatial partition pruning level ↑: −storage / +CPU sizing: requests ↔ egress

Algorithmic Compression & Entropy Profiling

Compression is the primary lever for reducing cold storage footprint. General-purpose algorithms rarely align with the structural characteristics of coordinate arrays, topology graphs, or categorical GIS attributes. Zstandard has emerged as the default for spatial workloads due to its tunable compression levels, dictionary support, and fast decompression. Applying a blanket compression level across heterogeneous datasets, however, wastes CPU cycles during archival or leaves storage savings on the table. Profiling coordinate variance, attribute cardinality, and temporal density lets teams assign an optimal compression tier per dataset class, ensuring predictable decompression throughput during cold retrieval. The full entropy-driven tuning matrices and CLI validation workflows live in ZSTD Level Configuration for Spatial Files.

# Train a ZSTD dictionary on a coordinate sample, then compress with it
zstd --train datasets/lidar/2023/coords_sample.bin -o dicts/spatial_dict.zdict
zstd -D dicts/spatial_dict.zdict -19 -c datasets/lidar/2023/raw_coords.bin \
  > archive/lidar/2023/compressed_coords.bin

A practical heuristic: profile before you pick a level. Coordinate columns whose low-order mantissa bits are effectively random gain almost nothing above level 12 and only burn CPU; categorical and temporal columns keep improving toward level 19. Splitting a dataset by column entropy and compressing each group at its own level is the single highest-leverage decision in the pipeline.

Coordinate Precision as a Compression Lever

Entropy profiling explains why coordinate columns resist compression; coordinate precision explains what to do about it. A double-precision longitude stored to fifteen decimal places carries roughly forty bits of mantissa that describe a position finer than any sensor on the platform could measure. Those bits are, statistically, noise: they never repeat, so no codec can model them, and they occupy the same bytes on every tier the object ever reaches. Rounding coordinates to the precision the data was actually captured at is the one transformation that makes the geometry column itself compressible rather than merely compressed.

The ladder below maps decimal places to ground resolution at the equator and to the compressed footprint of a 40 GB vector archive measured at each rounding step. Precision beyond six decimals buys nothing a cadastral surveyor would defend, yet costs a third of the archive:

Coordinate precision versus ground resolution and archive footprint Six rows of a precision ladder. Each row gives a decimal-place count, the ground resolution it resolves at the equator, and the compressed size of the same 40 gigabyte vector archive after rounding to that precision. Footprint falls steadily from 40.0 GB at fifteen decimals to 19.2 GB at four decimals, while ground resolution coarsens from 0.1 nanometres to 11 metres. A marker identifies six decimals — 11 centimetres — as the practical floor for survey-grade archives. decimals ground resolution compressed archive footprint 15 0.1 nm 40.0 GB 9 0.1 mm 33.4 GB 7 11 mm 27.1 GB 6 11 cm 24.6 GB 5 1.1 m 21.8 GB 4 11 m 19.2 GB survey-grade floor: 6 decimals keeps 39% of the saving with 11 cm error

Precision is a write-time decision with the same irreversibility as the retention lock: once rounded coordinates are archived and the Object Lock is applied, the discarded digits are gone for the life of the retention window. That makes the rounding threshold a governance question as much as a storage one — capture it in the dataset manifest alongside the codec level, and derive it from the survey accuracy recorded in the source metadata rather than from a convenient default. The full decision procedure, including how to prove that a rounding step did not move a feature across a jurisdictional boundary, is covered in Coordinate Precision & Encoding Strategies.

Columnar Layout & Row Group Architecture

Columnar formats like GeoParquet decouple geometry from attributes, enabling selective decompression and predicate pushdown. Yet the physical layout within those columns dictates cold retrieval efficiency. Row groups act as the fundamental unit of I/O in cloud object stores. Oversized groups increase memory pressure during partial scans and delay time-to-first-byte; undersized groups inflate metadata overhead, increase API request volume, and fragment compression dictionaries. The sizing model in Row Group Sizing Strategies aligns groups with typical cold-query scan windows (commonly 128–256 MB compressed per group) while respecting cloud storage chunk boundaries.

# PyArrow row group sizing for cold storage optimization
import pyarrow.parquet as pq

pq.write_table(
    geospatial_table,
    "s3://geo-archive/parquet/lidar/2023/region_north.parquet",
    row_group_size=1_000_000,   # rows per group, tuned toward a ~128 MB target
    compression="zstd",
    compression_level=19,
    use_dictionary=True,
    write_statistics=True,       # min/max stats enable row-group skipping
)

The write_statistics=True flag is what makes row groups useful for cold queries: per-group min/max statistics let an engine skip groups whose bounding values fall outside a spatial or temporal predicate, turning a full-file restore into a handful of ranged reads.

Attribute Encoding & Dictionary Optimization

Categorical fields — land use codes, sensor IDs, jurisdictional boundaries — dominate GIS attribute tables. When cardinality remains low, dictionary encoding drastically reduces storage overhead and accelerates equality predicates. High-cardinality fields, by contrast, degrade dictionary efficiency and increase decode latency. The cardinality thresholds and fallback strategies in Dictionary Encoding for GIS Attributes prevent decompression bottlenecks during compliance-driven attribute scans.

# Force dictionary encoding only on low-cardinality categorical columns,
# leaving high-cardinality IDs to plain ZSTD to avoid dictionary bloat.
import pyarrow.parquet as pq

pq.write_table(
    geospatial_table,
    "s3://geo-archive/parquet/parcels/2024/landuse.parquet",
    compression="zstd",
    use_dictionary=["land_use_code", "zoning_class", "jurisdiction"],
    column_encoding={"parcel_uuid": "PLAIN"},   # high cardinality: skip dictionary
    write_statistics=True,
)

The rule of thumb that drives the explicit list above: enable dictionary encoding when a column’s distinct-value count stays under roughly 10–20% of its row count, and disable it for unique identifiers where the dictionary would be as large as the data it replaces.

Spatial Partitioning & Physical Layout

Partitioning is the first line of defense against full-archive scans. Spatial partitioning techniques such as H3 hexagons, S2 cells, or Quadtree grids align physical file boundaries with geographic query extents. Combined with temporal partitioning (for example year/month), partition pruning eliminates the majority of unnecessary object retrievals before a single byte leaves cold storage. The implementation patterns in Spatial Partitioning Techniques reduce egress and request costs while keeping retrieval paths deterministic for audit trails.

# Partition a GeoParquet archive by H3 cell and year so cold queries
# prune to a bounded set of objects before any restore is issued.
import h3
import pyarrow.dataset as ds

def h3_partition(lat, lon, res=6):
    return h3.latlng_to_cell(lat, lon, res)

geospatial_table = geospatial_table.append_column(
    "h3_r6",
    [[h3_partition(lat, lon) for lat, lon in coords]],
)

ds.write_dataset(
    geospatial_table,
    base_dir="s3://geo-archive/parquet/sensors/",
    format="parquet",
    partitioning=ds.partitioning(
        flavor="hive", field_names=["h3_r6", "year"]
    ),
)

Partition resolution is itself a tuning decision: too coarse and each partition is a multi-gigabyte restore; too fine and metadata and small-object overhead dominate. Resolution 6–7 H3 cells map well to regional query extents for most archival workloads.

Compounding the Stages on a Real Sample

Every section above is measurable in isolation, which is exactly how teams end up disappointed: four settings each proven to help in a notebook, applied together to a production archive, deliver less than their sum because they interact. Dictionary encoding shrinks the attribute columns that ZSTD was already compressing well; row-group alignment changes what the codec sees inside each block; partition pruning does not shrink the archive at all — it shrinks the fraction of it a query ever pays to retrieve. The only defensible way to publish a savings number is to run the stages cumulatively over one representative partition and record the footprint after each.

The waterfall below is a real measurement pattern from a 100 TB municipal imagery-and-parcel archive: start at the uncompressed baseline, then apply each stage in the order the pipeline applies it.

Cumulative effect of each optimization stage on a 100 TB archive A waterfall chart. Bars fall from a 100 terabyte uncompressed baseline through ZSTD level 12 to 46 terabytes, coordinate rounding to 39, dictionary encoding to 34, and row-group alignment to 32 terabytes stored. A final separated bar shows partition pruning acting on the read path rather than on stored bytes, cutting the average query read to 0.9 terabytes. 100 TB 50 TB 0 100 TB baseline uncompressed 46 TB ZSTD 12 −54 TB 39 TB 6-decimal round −7 TB 34 TB dictionary −5 TB 32 TB row groups −2 TB stored 0.9 TB partition pruning per query, not stored right of the divider: read-path saving, not stored bytes

Two readings matter here. First, the codec does the heavy lifting exactly once — every later stage works on a much smaller number, so a team that skips profiling and settles for level 3 permanently caps what the rest of the pipeline can recover. Second, the last bar is a different currency: partition pruning leaves 32 TB in the bucket but means a typical bounded query restores under a terabyte, which is where the retrieval and egress line of the Spatial Archive Cost Modeling reference is actually won. Report both numbers separately; a single “we cut the archive by 68%” figure hides the lever that most affects the monthly bill.

Cross-Cutting Infrastructure & IaC Enforcement

Production readiness requires automated lifecycle transitions governed by infrastructure-as-code rather than console clicks. Storage-class transitions, retention windows, and compliance tags must be declared once and enforced continuously. The reference Terraform below transitions GeoParquet archives to Glacier Deep Archive after 90 days, scopes the rule to a prefix-and-tag filter, and applies an Object Lock so the data cannot be deleted inside its retention window:

resource "aws_s3_bucket_lifecycle_configuration" "spatial_cold_tier" {
  bucket = var.spatial_archive_bucket
  rule {
    id     = "geo-archive-to-deep-archive"
    status = "Enabled"
    transition {
      days          = 90
      storage_class = "DEEP_ARCHIVE"
    }
    # Combine a prefix and a tag with an `and` block.
    filter {
      and {
        prefix = "geospatial/parquet/"
        tags = {
          compliance_retention = "7y"
        }
      }
    }
  }
}

# Object Lock is its own resource, not a lifecycle sub-block.
resource "aws_s3_bucket_object_lock_configuration" "spatial_cold_tier" {
  bucket = var.spatial_archive_bucket
  rule {
    default_retention {
      mode = "GOVERNANCE"
      days = 2555 # ~7 years
    }
  }
}

Two cross-cutting realities shape these choices. First, egress and restore pricing dominate cold economics: Deep Archive storage is cheap, but bulk restores and egress are not, which is why the partitioning and row-group work above pays for itself by shrinking how much you ever retrieve. Second, vendor compatibility is not symmetric — Glacier Deep Archive, Azure Archive, and GCS Archive differ in minimum-storage-duration penalties and restore tiers, so the object-store decision documented under object storage selection for GIS archives should be made before compression parameters are frozen. For the authoritative tiering and restore-fee constraints, consult the AWS S3 lifecycle management documentation.

Compliance & Retention Integration

Compression and layout decisions intersect retention mandates more often than teams expect. Object Lock in GOVERNANCE or COMPLIANCE mode enforces immutability for windows set by mandates such as SEC Rule 17a-4 or GDPR retention limits, and those locks must survive any re-compression or re-partitioning job. That constraint means optimization is mostly a write-time decision: once an object is locked, you cannot rewrite it at a better compression level until its retention expires, so the tuning has to be correct before the lock is applied. Equally, partition boundaries should align with audit scopes — a legal-hold or jurisdiction-scoped audit becomes a single deterministic restore when partitioning follows the audit’s geographic and temporal seams instead of cutting across them. The retention policy frameworks section details how to express these windows as code, and metadata captured during conversion — including the source CRS preserved by CRS synchronization in pipelines — is what keeps a locked archive provably faithful to its source.

Operational Execution Checklist

Work through these steps when promoting a spatial dataset into optimized cold storage:

Conclusion

Cold storage optimization for geospatial data is not a static configuration but a continuous alignment of compression, layout, indexing, and governance. By profiling spatial entropy, enforcing row group boundaries, applying dictionary thresholds, and automating lifecycle transitions, organizations achieve predictable retrieval SLAs, audit-ready archives, and sustainable cost structures. For the format-level specification that underpins every decision above, consult the Apache Parquet documentation to ensure compliance across ingestion pipelines.

Frequently Asked Questions

Should compression be tuned before or after the data is converted to GeoParquet?

After. Codec level, dictionary thresholds, and row-group boundaries are all properties of the columnar writer, so they can only be set at the moment the GeoParquet file is written. Tune the conversion job itself rather than re-compressing afterwards: a second pass costs a full read and rewrite of the archive, and if the first copy was already tiered to Glacier, that pass also pays a restore and an early-deletion penalty. The one ordering rule that matters is that CRS normalisation and coordinate rounding happen upstream of the writer, because both change the byte distribution the codec sees.

How much CPU should compression be allowed to cost during archival?

Budget it against the archive’s write window rather than against a fixed rule. ZSTD level 12 compresses at roughly 25–40 MB/s per core on typical coordinate data, so a 40 TB nightly load needs about a hundred core-hours per terabyte-scale batch — trivially parallelised across an ingest fleet, and paid once. The asymmetry is the point: decompression stays near 500 MB/s per core regardless of the level used to write, so a higher level costs the archival job, never the restore path. The level only becomes a bad trade when it stops buying ratio, which for coordinate columns is typically somewhere around level 12 to 15.

Does compression interfere with Object Lock or retention compliance?

No, but it interacts with them in one direction. Compression is applied before the lock; once an object is under a COMPLIANCE-mode Object Lock it cannot be rewritten at all, so an under-tuned archive stays under-tuned for the full retention window. Auditors care that the archived bytes decode to the same features as the source, which is what the conversion parity gate proves, not which codec produced them. Record the codec, level, and coordinate precision in the dataset manifest so a future auditor can reproduce the file exactly.

Why do two partitions of the same dataset compress to different ratios?

Almost always because their coordinate distributions differ. A partition covering dense urban parcels carries many similar coordinate prefixes and compresses well; a sparse rural partition of the same schema does not. The other common cause is mixed CRS across partitions — one region written in a projected system and another in geographic degrees present completely different byte patterns to the codec. Normalising the reference system upstream removes that variance, after which residual differences are genuine data-density differences and not a configuration fault.

What object size should an optimized cold archive aim for?

Between roughly 256 MB and 4 GB per object for archive tiers. Below that, per-request restore charges and per-object metadata overhead start to rival the storage saving — a million 10 MB objects cost far more to restore than ten thousand 1 GB ones holding the same bytes, and Deep Archive adds a per-object overhead charge on top. Above a few gigabytes, any query that needs a slice of the data pays to restore the whole object, which undoes the pruning work. The partition resolution and row-group targets in this guide are chosen to land naturally in that window: an H3 resolution-6 partition of a national parcel dataset written in 128 MB row groups typically produces objects between 400 MB and 2 GB.

Is it worth compressing data that is already going to Deep Archive?

Yes, and more so than for hot data. Deep Archive bills per stored gigabyte for a minimum of 180 days, so every gigabyte removed at write time is removed from six months of billing at minimum. Compression also shrinks the restore itself: bulk retrieval is priced per gigabyte retrieved, so a 3:1 ratio cuts both the standing storage line and the cost of every future restore. The only case where aggressive compression does not pay is a small archive whose per-request costs dominate its per-gigabyte costs, which the object-count guidance under row-group and partition sizing is designed to prevent.

Up one level: Spatial Data Archival & Cold Storage Optimization.