Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy-zoomies

osm-katana/tests/rung15_measure.rs

//! **Row 23, the MEASUREMENT — what does the z13–z15 city camera actually pay, and what
//! would each candidate fix buy?**
//!
//! # Why this runs before any code changes
//!
//! `GENERALISED_RUNGS` is `[6, 9, 12]`, so a camera at lod 13–15 has no rung column and
//! reads full-precision `geometry`: **25.64 MB / 365 163 ways / 1.64 M vertices / 241 ms
//! at Stockholm** (audit A, 2026-09-05). Two fixes are on the table and they are not
//! equivalent:
//!
//! * **add rung 15** — a fourth derived column, Douglas–Peucker at
//!   `360/(2048·2^15)` ≈ 5.4e-6° ≈ **0.6 m**.
//! * **re-encode as i32 microdegrees** — 8 bytes per vertex instead of 16, at a
//!   quantisation of 1e-6° ≈ **0.11 m**, which is *finer than the z15 epsilon itself*.
//!
//! `shared::schema::GENERALISED_RUNGS`' own doc asserts the first one is worthless —
//! *"running Douglas–Peucker at that tolerance over Swedish OSM geometry moves almost
//! nothing while costing … a byte-for-byte duplicate of the largest column in the file"*
//! — but that sentence carries **no number**, and it was written before `ff1cd6d` made
//! the kernel metric. The cos(lat) correction changes exactly this: at 60°N a
//! north–south deviation is now scaled by 0.5, so z15 DP removes *more* than it did when
//! the claim was made. A claim with no measurement, invalidated by a later commit, is
//! not a reason to skip a fix — so this measures it.
//!
//! Read-only against the live Sweden cut. It writes nothing and swaps nothing.
//!
//! Run: `cargo test -p osm-katana --release --test rung15_measure -- --ignored --nocapture`

use std::path::Path;

use arrow::array::{Array, BinaryArray, UInt8Array};
use parquet::arrow::ProjectionMask;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;

const LIVE: &str = "/home/korp/.korp/work/sweden/ways.parquet";

/// Central Stockholm at lod 14 — the camera the audit priced.
const VIEW: (f64, f64, f64, f64) = (17.95, 59.28, 18.20, 59.40);

/// The quantisation an i32-microdegree encoding would impose, in degrees.
const MICRODEGREE: f64 = 1e-6;

/// A WKB blob re-encoded with i32 microdegree coordinates instead of f64 pairs.
///
/// Only the SIZE is computed, not the bytes: the question here is what the wire would
/// weigh, and building a second encoder before knowing whether it is worth having is the
/// thing this measurement exists to avoid.
///
/// The layout priced is the honest one — the same headers WKB already carries (1 byte
/// order + 4 type + 4 count per geometry, recursively) with each coordinate PAIR going
/// from 16 bytes to 8.
fn microdegree_bytes(wkb: &[u8], verts: usize, geoms: usize) -> usize {
    // Every header byte survives; only the coordinate payload halves.
    let coord_bytes = verts * 16;
    let headers = wkb.len().saturating_sub(coord_bytes);
    headers + verts * 8 + geoms * 0
}

/// Vertices and sub-geometry count of a decoded shape.
fn shape_size(wkb: &[u8]) -> Option<(usize, usize)> {
    let shape = osm_katana::shared::geometry::decode_wkb(wkb)?;
    let mut verts = 0usize;
    let mut geoms = 0usize;
    match &shape {
        osm_katana::shared::geometry::WkbShape::Point(_) => {
            verts += 1;
            geoms += 1;
        }
        osm_katana::shared::geometry::WkbShape::Lines(runs) => {
            geoms += runs.len();
            for r in runs {
                verts += r.len();
            }
        }
        osm_katana::shared::geometry::WkbShape::Polys(polys) => {
            for (outer, holes) in polys {
                geoms += 1 + holes.len();
                verts += outer.len();
                for h in holes {
                    verts += h.len();
                }
            }
        }
    }
    Some((verts, geoms))
}

fn bbox_of(wkb: &[u8]) -> Option<(f64, f64, f64, f64)> {
    let c = osm_katana::shared::geometry::decode_coords(wkb)?;
    if c.is_empty() {
        return None;
    }
    let (mut x0, mut y0, mut x1, mut y1) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    for (x, y) in c {
        x0 = x0.min(x);
        y0 = y0.min(y);
        x1 = x1.max(x);
        y1 = y1.max(y);
    }
    Some((x0, y0, x1, y1))
}

fn intersects(b: (f64, f64, f64, f64)) -> bool {
    !(b.2 < VIEW.0 || b.0 > VIEW.2 || b.3 < VIEW.1 || b.1 > VIEW.3)
}

/// ★ **The number row 23's decision turns on.** Not a gate — a measurement, printed.
#[test]
#[ignore = "reads korp's live Sweden cut; run with --ignored --release"]
fn what_the_city_camera_pays_and_what_each_fix_would_save() {
    let p = Path::new(LIVE);
    assert!(p.exists(), "{LIVE} is not on this box");

    let file = std::fs::File::open(p).unwrap();
    let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
    let schema = builder.parquet_schema();
    // Project only what is needed: `geometry` and `min_zoom`. Pulling `tags` would
    // dominate the read and has nothing to do with the question.
    let mut leaves = Vec::new();
    for (i, col) in schema.columns().iter().enumerate() {
        let n = col.path().string();
        if n == "geometry" || n == "min_zoom" {
            leaves.push(i);
        }
    }
    let mask = ProjectionMask::leaves(schema, leaves);
    let reader = builder.with_projection(mask).with_batch_size(8_192).build().unwrap();

    let eps15 = osm_katana::shared::generalise::generalisation_eps(15);
    let eps12 = osm_katana::shared::generalise::generalisation_eps(12);

    let mut rows = 0usize;
    let (mut v_full, mut b_full) = (0usize, 0usize);
    let (mut v_z15, mut b_z15) = (0usize, 0usize);
    let mut b_micro = 0usize;
    let mut decode_fail = 0usize;

    for batch in reader {
        let batch = batch.unwrap();
        let g = batch
            .column_by_name("geometry")
            .unwrap()
            .as_any()
            .downcast_ref::<BinaryArray>()
            .unwrap();
        let mz = batch
            .column_by_name("min_zoom")
            .unwrap()
            .as_any()
            .downcast_ref::<UInt8Array>()
            .unwrap();
        for i in 0..batch.num_rows() {
            if g.is_null(i) || mz.is_null(i) {
                continue;
            }
            // What a lod-14 camera admits: the rung must be at or below the camera.
            if mz.value(i) > 14 {
                continue;
            }
            let w = g.value(i);
            let Some(bb) = bbox_of(w) else { continue };
            if !intersects(bb) {
                continue;
            }
            rows += 1;
            let Some((verts, geoms)) = shape_size(w) else {
                decode_fail += 1;
                continue;
            };
            v_full += verts;
            b_full += w.len();
            b_micro += microdegree_bytes(w, verts, geoms);

            if let Some(s15) = osm_katana::shared::generalise::generalise_wkb(w, eps15) {
                if let Some((v, _)) = shape_size(&s15) {
                    v_z15 += v;
                }
                b_z15 += s15.len();
            } else {
                v_z15 += verts;
                b_z15 += w.len();
            }
        }
    }

    let pct = |a: usize, b: usize| {
        if b == 0 { 0.0 } else { 100.0 * (b as f64 - a as f64) / b as f64 }
    };
    println!("\n── row 23 · lod 14 Stockholm, live cut, min_zoom <= 14 ──");
    println!("  rows in view          {rows}   (decode failures {decode_fail})");
    println!("  z15 eps               {eps15:.4e}° ≈ {:.2} m", eps15 * 111_320.0);
    println!("  z12 eps               {eps12:.4e}° ≈ {:.2} m", eps12 * 111_320.0);
    println!("  microdegree quantum   {MICRODEGREE:.0e}° ≈ {:.3} m", MICRODEGREE * 111_320.0);
    println!("  vertices  full        {v_full}");
    println!("  vertices  after z15   {v_z15}   ({:.1} % removed)", pct(v_z15, v_full));
    println!("  BYTES     full        {b_full}  ({:.2} MB)", b_full as f64 / 1e6);
    println!("  BYTES     z15 column  {b_z15}  ({:.2} MB, {:.1} % saved)", b_z15 as f64 / 1e6, pct(b_z15, b_full));
    println!("  BYTES     microdeg    {b_micro}  ({:.2} MB, {:.1} % saved)", b_micro as f64 / 1e6, pct(b_micro, b_full));
    println!(
        "  BYTES     both        {}  ({:.2} MB, {:.1} % saved)",
        b_z15 / 2,
        (b_z15 / 2) as f64 / 1e6,
        pct(b_z15 / 2, b_full)
    );
    println!("── the row's gate is >= 40 % ──\n");

    assert!(rows > 1_000, "only {rows} rows in the viewport — the projection or the bbox is wrong");
}

/// **The number that actually matters: COMPRESSED bytes off the footer.**
///
/// The measurement above prices *serialized* WKB. What a camera pulls is the
/// **zstd-compressed column chunk**, and the two are not proportional — a `f64`
/// coordinate whose low mantissa bits are noise is nearly incompressible, and the same
/// coordinate rounded to the rung's own precision is not.
///
/// That opens a third candidate the row's brief does not name, and it is the one that
/// costs no reader change anywhere:
///
/// | candidate | what it is | who has to change |
/// |---|---|---|
/// | **A** rung 15 | a fourth derived column, DP at 0.6 m | osm-katana + korp's four-variant level enum |
/// | **B** i32 microdegrees | 8 bytes/vertex instead of 16 | osm-katana **and korp's WKB decoder** |
/// | **C** quantise, keep f64 WKB | round coordinates to 1e-6°, still valid WKB | osm-katana **only** |
///
/// C is lossless against the rung it serves — 1e-6° is 0.11 m and the z15 epsilon is
/// 0.60 m, so the quantum is five times finer than the tolerance the column is cut at.
/// It is measured here rather than argued, because "zstd will like it" is exactly the
/// kind of claim `GENERALISED_RUNGS`' own doc made without a number.
#[test]
#[ignore = "reads korp's live Sweden cut; run with --ignored --release"]
fn what_each_candidate_costs_in_bytes_actually_pulled() {
    use std::sync::Arc;

    use arrow::array::{ArrayRef, BinaryBuilder};
    use arrow::datatypes::{DataType, Field, Schema};
    use arrow::record_batch::RecordBatch;

    let p = Path::new(LIVE);
    assert!(p.exists(), "{LIVE} is not on this box");

    // Collect the viewport's geometry once.
    let file = std::fs::File::open(p).unwrap();
    let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
    let schema = builder.parquet_schema();
    let leaves: Vec<usize> = schema
        .columns()
        .iter()
        .enumerate()
        .filter(|(_, c)| {
            let n = c.path().string();
            n == "geometry" || n == "min_zoom"
        })
        .map(|(i, _)| i)
        .collect();
    let mask = ProjectionMask::leaves(schema, leaves);
    let reader = builder.with_projection(mask).with_batch_size(8_192).build().unwrap();

    let mut geoms: Vec<Vec<u8>> = Vec::new();
    for batch in reader {
        let batch = batch.unwrap();
        let g = batch.column_by_name("geometry").unwrap().as_any().downcast_ref::<BinaryArray>().unwrap();
        let mz = batch.column_by_name("min_zoom").unwrap().as_any().downcast_ref::<UInt8Array>().unwrap();
        for i in 0..batch.num_rows() {
            if g.is_null(i) || mz.is_null(i) || mz.value(i) > 14 {
                continue;
            }
            let w = g.value(i);
            if bbox_of(w).is_some_and(intersects) {
                geoms.push(w.to_vec());
            }
        }
    }
    assert!(geoms.len() > 1_000, "only {} rows collected", geoms.len());

    let eps15 = osm_katana::shared::generalise::generalisation_eps(15);

    /// Round every coordinate of a WKB blob to the microdegree grid, in place, keeping
    /// the format byte-for-byte identical in LAYOUT. Decode/re-encode through the
    /// crate's own codec so this cannot quietly produce something the reader would
    /// reject.
    fn requantise(wkb: &[u8], quantise: bool) -> Option<Vec<u8>> {
        use osm_katana::shared::geometry::{WkbShape, decode_wkb, encode_wkb_multilinestring_into, encode_wkb_multipolygon_into, encode_wkb_point};
        let q = |v: f64| if quantise { (v * 1e6).round() / 1e6 } else { v };
        Some(match decode_wkb(wkb)? {
            WkbShape::Point(p) => encode_wkb_point(q(p.0), q(p.1)),
            WkbShape::Lines(runs) => {
                let r: Vec<Vec<(f64, f64)>> =
                    runs.iter().map(|r| r.iter().map(|&(x, y)| (q(x), q(y))).collect()).collect();
                let mut b = Vec::new();
                encode_wkb_multilinestring_into(&r, &mut b);
                b
            }
            WkbShape::Polys(polys) => {
                let ps: Vec<(Vec<(f64, f64)>, Vec<Vec<(f64, f64)>>)> = polys
                    .iter()
                    .map(|(o, hs)| {
                        (
                            o.iter().map(|&(x, y)| (q(x), q(y))).collect(),
                            hs.iter().map(|h| h.iter().map(|&(x, y)| (q(x), q(y))).collect()).collect(),
                        )
                    })
                    .collect();
                let mut b = Vec::new();
                encode_wkb_multipolygon_into(&ps, &mut b);
                b
            }
        })
    }

    /// Write one geometry column with the packer's own compression and row-group size,
    /// and report the compressed bytes of that column off the footer.
    fn compressed_bytes(blobs: &[Vec<u8>], name: &str) -> u64 {
        let dir = std::env::temp_dir().join(format!("kd-row23-{}-{}", name, std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let path = dir.join(format!("{name}.parquet"));
        let schema = Arc::new(Schema::new(vec![Field::new("geometry", DataType::Binary, true)]));
        let props = parquet::file::properties::WriterProperties::builder()
            .set_compression(parquet::basic::Compression::ZSTD(Default::default()))
            .set_max_row_group_row_count(Some(8_192))
            .build();
        let f = std::fs::File::create(&path).unwrap();
        let mut w = parquet::arrow::ArrowWriter::try_new(f, schema.clone(), Some(props)).unwrap();
        for chunk in blobs.chunks(8_192) {
            let mut b = BinaryBuilder::new();
            for g in chunk {
                b.append_value(g);
            }
            let cols: Vec<ArrayRef> = vec![Arc::new(b.finish())];
            w.write(&RecordBatch::try_new(schema.clone(), cols).unwrap()).unwrap();
        }
        w.close().unwrap();
        let r = parquet::file::serialized_reader::SerializedFileReader::new(
            std::fs::File::open(&path).unwrap(),
        )
        .unwrap();
        let md = parquet::file::reader::FileReader::metadata(&r);
        let mut bytes = 0u64;
        for i in 0..md.num_row_groups() {
            for c in md.row_group(i).columns() {
                bytes += c.compressed_size() as u64;
            }
        }
        let _ = std::fs::remove_dir_all(&dir);
        bytes
    }

    let base = compressed_bytes(&geoms, "base");
    // **The CONTROL, and it has to come first.** `requantise` decodes and re-encodes
    // through this crate's own writers, and those always emit the Multi* spelling — so a
    // round trip changes the geometry TYPE and adds a wrapper header per row whether or
    // not a single coordinate moves. Without this arm, that cost would be attributed to
    // the quantisation and the row would draw the opposite conclusion from the truth.
    let roundtrip: Vec<Vec<u8>> = geoms.iter().map(|g| requantise(g, false).unwrap_or_else(|| g.clone())).collect();
    let c_rt = compressed_bytes(&roundtrip, "roundtrip");
    let quant: Vec<Vec<u8>> = geoms.iter().map(|g| requantise(g, true).unwrap_or_else(|| g.clone())).collect();
    assert_eq!(quant.len(), geoms.len(), "requantise() dropped rows");
    let c_quant = compressed_bytes(&quant, "quant");
    let dp15: Vec<Vec<u8>> = geoms
        .iter()
        .map(|g| osm_katana::shared::generalise::generalise_wkb(g, eps15).unwrap_or_else(|| g.clone()))
        .collect();
    let c_dp = compressed_bytes(&dp15, "dp15");
    let both: Vec<Vec<u8>> = dp15.iter().map(|g| requantise(g, true).unwrap_or_else(|| g.clone())).collect();
    let c_both = compressed_bytes(&both, "both");

    let pct = |a: u64| 100.0 * (base as f64 - a as f64) / base as f64;
    println!("\n── row 23 · COMPRESSED bytes, lod 14 Stockholm, {} rows ──", geoms.len());
    println!("  base   `geometry` as served      {base:>10}  ({:.2} MB)", base as f64 / 1e6);
    println!("  A      DP at z15 (0.60 m)        {c_dp:>10}  ({:.2} MB, {:+.1} %)", c_dp as f64 / 1e6, -pct(c_dp));
    println!("  C0 CTRL re-encode, NO quantise   {c_rt:>10}  ({:.2} MB, {:+.1} %)", c_rt as f64 / 1e6, -pct(c_rt));
    println!("  C      quantised to 1e-6 deg     {c_quant:>10}  ({:.2} MB, {:+.1} %)", c_quant as f64 / 1e6, -pct(c_quant));
    println!("  A+C    both                      {c_both:>10}  ({:.2} MB, {:+.1} %)", c_both as f64 / 1e6, -pct(c_both));

    // ── WHY C went the wrong way, and what B actually costs ────────────────────
    //
    // A guess would be worthless here, so: what GRID are the served coordinates
    // already on? OSM stores fixed-point nanodegrees, and if the cut's f64s are exact
    // multiples of 1e-7 their low mantissa bits are a repeating pattern zstd already
    // exploits — in which case rounding through `(v*1e6).round()/1e6` does not
    // "quantise" anything, it replaces a clean pattern with the arbitrary mantissa of a
    // division. That would explain a candidate making the column BIGGER.
    let mut on_1e7 = 0usize;
    let mut coords_checked = 0usize;
    for g in geoms.iter().take(5_000) {
        if let Some(cs) = osm_katana::shared::geometry::decode_coords(g) {
            for (x, y) in cs {
                for v in [x, y] {
                    coords_checked += 1;
                    if ((v * 1e7).round() - v * 1e7).abs() < 1e-3 {
                        on_1e7 += 1;
                    }
                }
            }
        }
    }
    println!(
        "  grid check: {on_1e7} of {coords_checked} coordinates ({:.2} %) sit on the 1e-7 nanodegree grid",
        100.0 * on_1e7 as f64 / coords_checked.max(1) as f64
    );

    /// **Candidate B** — the coordinates as i32 microdegrees instead of f64 pairs.
    ///
    /// Two spellings, because "8 bytes instead of 16" is only the floor: a coordinate
    /// list is a PATH, so consecutive vertices differ by very little, and a
    /// delta+zigzag+varint payload is what that fact is worth. Both are measured
    /// because the cheap one may already clear the gate and is far less code.
    ///
    /// It reads its coordinates through `decode_wkb`, **not** `decode_coords`. That is
    /// not a style choice: `decode_coords` returned `None` for **64 %** of the
    /// z15-simplified blobs (35 119 of 98 080 survived a `filter_map`), which silently
    /// priced the combined candidate over a third of its population and produced a
    /// −77.7 % that was not real. `decode_wkb` decodes all 98 080 of both sets.
    fn micro_bytes(wkb: &[u8], delta: bool) -> Option<Vec<u8>> {
        use osm_katana::shared::geometry::WkbShape;
        let cs: Vec<(f64, f64)> = match osm_katana::shared::geometry::decode_wkb(wkb)? {
            WkbShape::Point(p) => vec![p],
            WkbShape::Lines(runs) => runs.into_iter().flatten().collect(),
            WkbShape::Polys(polys) => polys
                .into_iter()
                .flat_map(|(o, hs)| o.into_iter().chain(hs.into_iter().flatten()))
                .collect(),
        };
        let mut out = Vec::with_capacity(cs.len() * 8);
        // The same headers WKB carries; only the coordinate payload is re-spelled, so
        // this is an honest price for the column and not for a different schema.
        out.extend_from_slice(&wkb[..9.min(wkb.len())]);
        let (mut px, mut py) = (0i32, 0i32);
        for (x, y) in cs {
            let xi = (x * 1e6).round() as i32;
            let yi = (y * 1e6).round() as i32;
            if delta {
                for d in [xi.wrapping_sub(px), yi.wrapping_sub(py)] {
                    // zigzag + LEB128
                    let mut z = ((d << 1) ^ (d >> 31)) as u32;
                    loop {
                        let b = (z & 0x7f) as u8;
                        z >>= 7;
                        if z == 0 {
                            out.push(b);
                            break;
                        }
                        out.push(b | 0x80);
                    }
                }
                px = xi;
                py = yi;
            } else {
                out.extend_from_slice(&xi.to_le_bytes());
                out.extend_from_slice(&yi.to_le_bytes());
            }
        }
        Some(out)
    }

    let b1: Vec<Vec<u8>> = geoms.iter().map(|g| micro_bytes(g, false).unwrap_or_else(|| g.clone())).collect();
    let b2: Vec<Vec<u8>> = geoms.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
    let b1_dp: Vec<Vec<u8>> = dp15.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
    // A `filter_map` that silently dropped rows would make every ratio below a
    // comparison between different populations — the shape of defect this file exists
    // to avoid, so it is refused rather than trusted.
    assert_eq!(b1.len(), geoms.len(), "micro_bytes dropped rows from the base set");
    assert_eq!(b2.len(), geoms.len(), "micro_bytes dropped rows from the base set");
    assert_eq!(b1_dp.len(), dp15.len(), "micro_bytes dropped rows from the DP set");
    assert_eq!(dp15.len(), geoms.len(), "the DP set is not the base set");
    // And the vertex populations, so a byte ratio can be read against the vertex ratio
    // rather than admired on its own.
    // `shape_size` uses `decode_wkb`, which decodes both sets completely — counting
    // through `decode_coords` here would repeat the very row loss asserted against above.
    let vcount = |set: &[Vec<u8>]| -> usize {
        set.iter().filter_map(|g| shape_size(g)).map(|(v, _)| v).sum()
    };
    let (v_base, v_dp) = (vcount(&geoms), vcount(&dp15));
    println!(
        "  vertices: base {v_base} -> after z15 DP {v_dp}  ({:.1} % removed)",
        100.0 * (v_base as f64 - v_dp as f64) / v_base as f64
    );
    let c_b1 = compressed_bytes(&b1, "b1");
    let c_b2 = compressed_bytes(&b2, "b2");
    let c_b2dp = compressed_bytes(&b1_dp, "b2dp");
    println!("  B1     i32 microdeg, absolute    {c_b1:>10}  ({:.2} MB, {:+.1} %)", c_b1 as f64 / 1e6, -pct(c_b1));
    println!("  B2     i32 microdeg, delta+varint{c_b2:>10}  ({:.2} MB, {:+.1} %)", c_b2 as f64 / 1e6, -pct(c_b2));
    println!("  A+B2   DP at z15, then delta     {c_b2dp:>10}  ({:.2} MB, {:+.1} %)", c_b2dp as f64 / 1e6, -pct(c_b2dp));

    // **The rung the camera actually wants.** z15's epsilon (0.60 m) is FINER than one
    // lod-14 pixel (1.19 m at the 2048 raster), so candidate A is simplifying to a
    // tolerance the camera cannot resolve. Price the coarser neighbours too, or the row
    // decides between two options without knowing what the third costs.
    for z in [13u8, 14] {
        let e = osm_katana::shared::generalise::generalisation_eps(z);
        let set: Vec<Vec<u8>> = geoms
            .iter()
            .map(|g| osm_katana::shared::generalise::generalise_wkb(g, e).unwrap_or_else(|| g.clone()))
            .collect();
        let c = compressed_bytes(&set, &format!("dp{z}"));
        let d: Vec<Vec<u8>> = set.iter().map(|g| micro_bytes(g, true).unwrap_or_else(|| g.clone())).collect();
        let cd = compressed_bytes(&d, &format!("dp{z}d"));
        println!(
            "  A(z{z})  DP at {:.2} m               {c:>10}  ({:.2} MB, {:+.1} %)   +delta {:.2} MB ({:+.1} %)  verts {}",
            e * 111_320.0,
            c as f64 / 1e6,
            -pct(c),
            cd as f64 / 1e6,
            -pct(cd),
            vcount(&set),
        );
    }
    // **Is the city camera even paying in BYTES?** The audit priced this viewport at
    // 241 ms. 4.06 MB off a local NVMe is not 241 ms, so before this row spends a wire
    // format on a 20 % byte saving, measure what the time is actually made of.
    let t = std::time::Instant::now();
    let mut sink = 0usize;
    for g in &geoms {
        if let Some((v, _)) = shape_size(g) {
            sink += v;
        }
    }
    let decode_base = t.elapsed();
    let dp13: Vec<Vec<u8>> = geoms
        .iter()
        .map(|g| {
            osm_katana::shared::generalise::generalise_wkb(g, osm_katana::shared::generalise::generalisation_eps(13))
                .unwrap_or_else(|| g.clone())
        })
        .collect();
    let t = std::time::Instant::now();
    let mut sink13 = 0usize;
    for g in &dp13 {
        if let Some((v, _)) = shape_size(g) {
            sink13 += v;
        }
    }
    let decode_13 = t.elapsed();
    println!(
        "  DECODE  base {sink} verts in {:.1} ms   ·   z13 {sink13} verts in {:.1} ms  ({:+.1} %)",
        decode_base.as_secs_f64() * 1e3,
        decode_13.as_secs_f64() * 1e3,
        -100.0 * (decode_base.as_secs_f64() - decode_13.as_secs_f64()) / decode_base.as_secs_f64(),
    );
    println!("── the row's gate is >= 40 % saved ──\n");
}