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

nordisk/facett

facett-map/src/style.rs

//! Per-OSM-tag styling for the vector basemap, ported from katana-osm's
//! `osm-viewer/src/render/style.rs` and re-keyed onto the facett [`Theme`].
//!
//! The source bakes literal RGB into each style; facett instead derives the
//! stroke + fill colours from the active [`Theme`] palette so a way layer
//! re-themes with the rest of the deck. Two big additions over the original
//! line-only port make the basemap **look like a real map** instead of bare
//! coloured lines:
//!
//! 1. **area-vs-line** ([`WayStyle::is_area`]) — a closed ring tagged
//!    `natural=water`/`waterway`/`landuse`/`leisure=park`/`natural=wood`/
//!    `building`/`amenity` is an **area** (polygon → fill); everything else
//!    (roads, paths, coastline) is a **line** (stroke). The reader
//!    ([`crate::Way::from_lonlat`]) only turns an area into a filled silhouette
//!    when its geometry actually closes (a ring), so a `landuse` drawn as an
//!    open way still strokes gracefully.
//! 2. **fill colour** ([`WayStyle::fill`]) — themed blue (water), green
//!    (parks / forest / landuse) or a subtle grey (buildings), so the areas
//!    paint as filled polygons under the roads.
//!
//! The **level-of-detail** tiers (country / region / city) are carried verbatim
//! — they drive the GPU cull's cumulative LOD test.

use egui::Color32;
use facett_core::Theme;

use crate::WayKind;

/// Level-of-detail tier. Country ways show at every zoom; city ways only when zoomed
/// in; **detail** ways only at street level. The GPU cull compares `lod <= viewport_lod`.
///
/// # Why there is a fourth tier (`kalm-1`)
///
/// The three-tier ladder came verbatim from osm-viewer, and it had no room for the
/// footpath network — `highway=footway|cycleway|path|pedestrian|track|steps` sat at
/// [`Lod::City`], **the same tier as a residential street**, so from zoom 12 upward
/// every footpath in the viewport drew with the same right as an arterial.
///
/// The measurement that says it is a defect and not a taste: on the day palette the
/// path brown `#B99873` is **2.35:1** against the paper (`tools/mapdrive/ink.py`,
/// WCAG), the highest-contrast class on the whole map after the coastline — against
/// 1.20:1 for a building and 1.02:1 for a residential street. At camera 12.7 the pane
/// is ~1.7 px per 20 m, so a 2 m path is a **third of a pixel** wide. The loudest ink
/// on the map, drawn at a width where it can only be grain.
///
/// `Detail` is the room to put it in, and it costs nothing on the GPU: `cull.wgsl`
/// tests `m.lod > vp.lod`, a scalar compare. The three-entry `line_lod_end` prefix
/// table that a fourth tier was thought to need a shader change for is a **retained
/// fact** — the indirect draw stopped reading it when the compute cull landed
/// (`gpu/mod.rs`, "What this replaced: `0..line_lod_end[lod]`").
///
/// # Why there is a fifth tier (`District`, the z9 rung)
///
/// The served pyramid is `[6, 9, 12, 15]` and the ladder was `[0, 6, 12, 15]`. **z9 was
/// cut, stored, shipped over the wire, decoded — and then drew the same tier z6 draws.**
/// A camera anywhere in `9..12` paid for a level that changed nothing on screen.
///
/// korp's cross-seam law (`korp/tests/lod_one_threshold.rs`,
/// `fetch_level_and_draw_tier_step_together`) could not see it: it asserts that fetch
/// and draw never step without each other, and here they agreed — about ignoring the
/// rung. `facett-map/tests/lod_ladder_vs_pyramid.rs` is the other half of that
/// law and is where this is now measured. (Both citations here said
/// `lod_ladder_serves_its_pyramid.rs` until 2026-08-27 -- a filename the repo has
/// never had, so a reader who followed the law to its guard found nothing. The
/// guard itself is real and always was: `the_two_ladders_step_together_at_every_rung`
/// and `every_tier_step_lands_on_a_rung_the_pyramid_serves`.)
///
/// **z9 and not z8/z10/z13**: the rung has to be one the pyramid already serves, or the
/// tier renders the coarser data and buys nothing all over again. z9 is free; the others
/// each need a 44 GB re-cut first.
///
/// **What moved, and the accepted cost.** `Lod::Region` held `secondary` AND `tertiary`
/// roads and the canal; tertiary and canal move here. The owner's decision, taken with the
/// consequence stated: **tertiary roads no longer draw between camera 6 and camera 9**,
/// where they draw today. That is the point rather than a side effect — a tier that
/// renders the same set as the tier above it is the "exists in the type and nowhere on
/// screen" defect [`LOD_COUNT`] warns about, reached from the other side. `secondary`
/// deliberately stays at `Region`.
///
/// # The ladder gates DRAWING ONLY — measured, not assumed
///
/// A tier that withholds a road class would be a **correctness** change rather than a
/// look change if anything traversed the ways as a graph, because connectivity spans
/// tile boundaries and a per-zoom filter cannot see the seam it cuts. Nothing does, and
/// the evidence is specific:
///
/// * `Map3D::set_route` (`facett-map3d/src/lib.rs`) takes its `waypoints` from the
///   CALLER. `extrude::project_route` maps each one through `to_design` independently —
///   no adjacency, no snapping, no search. Its `ways` argument feeds only
///   `footprint_bbox` (the coordinate frame) and `design_units_per_metre` (the scale),
///   both LOD-independent aggregates.
/// * `extrude::street_label_anchors` iterates **every** way, skipping only non-roads,
///   unnamed roads and degenerate geometry. It STORES `lod: w.style.lod` per anchor; the
///   value is read at HUD paint time and nowhere else. Turn-by-turn naming
///   (`maneuver::road_names_along`) reads that full, unfiltered set.
/// * `MapIndex` PARTITIONS ways into per-tier vectors and retains all of them
///   (`lod_sizes`); `layer::lod_visible` is a paint-time predicate, not a filter that
///   drops.
///
/// So the tier is a display gate. A future lane that adds a traversing consumer — a real
/// router, a connectivity check — must read the untiered set, and this paragraph is the
/// record that the question was asked rather than assumed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Lod {
    Country = 0,
    Region = 1,
    /// **The z9 rung** — the sub-regional network: `highway=tertiary` and
    /// `waterway=canal`. See "Why there is a fifth tier" above.
    District = 2,
    City = 3,
    /// Street-level furniture: the footpath network. Drawn only once the camera stands
    /// on the deepest pyramid rung, where a path is metres wide in pixels.
    Detail = 4,
}

/// How many [`Lod`] tiers there are — **the one writer** for every per-tier array in
/// this crate (`MapIndex`'s tier vectors and grids, the GPU bake's `line_polys_by_lod`
/// and `line_lod_end`). Those were three independent literal `3`s and a `.min(2)`, and
/// a fourth variant added to the enum alone would have folded silently into the third
/// at every one of them — a tier that exists in the type and nowhere on screen.
pub const LOD_COUNT: usize = 5;

impl Lod {
    /// Every tier, ascending.
    pub const ALL: [Lod; LOD_COUNT] =
        [Lod::Country, Lod::Region, Lod::District, Lod::City, Lod::Detail];

    /// The tier a `u32` from the wire/GPU names, saturating at the deepest tier.
    #[must_use]
    pub fn from_u32(v: u32) -> Lod {
        Self::ALL[(v as usize).min(LOD_COUNT - 1)]
    }
}

/// A resolved way style: stroke colour + width + LOD tier + classification, plus
/// the **area** flag and optional **fill** colour that make closed rings paint as
/// filled polygons (a real map) rather than bare outlines.
#[derive(Clone, Copy, Debug)]
pub struct WayStyle {
    /// Stroke (outline / line) colour.
    pub color: Color32,
    /// Stroke width in logical px (at fit-zoom; the painter scales with zoom).
    pub width: f32,
    pub lod: Lod,
    pub kind: WayKind,
    /// `true` → this way represents an **area** (water body, park, building,
    /// landuse): when its geometry is a closed ring it is painted as a filled
    /// polygon. `false` → a **line** (road, path, coastline): stroked only.
    pub is_area: bool,
    /// Themed fill colour for an area way (`None` for lines). Set ⇔ `is_area`.
    pub fill: Option<Color32>,
}

/// The map's cartographic palette — one concrete colour per feature class. Two
/// sources feed it:
///
/// * [`MapPalette::from_theme`] derives every colour off the active [`Theme`] so
///   the basemap re-themes with the rest of the deck (the historical behaviour;
///   selected when the view is `themeable`).
/// * [`MapPalette::cartographic`] returns a **concrete-hex** preset (a real map's
///   paper/water/green/road-tier hues, day + night) — a fixed cartographic look
///   selectable *alongside* the theme-derived one, not blended off the theme.
///
/// The road-tier + `casing` fields let a map paint each `highway` class its own
/// hue and draw a dark **casing** (outline) under the bright road fill.
#[derive(Clone, Copy, Debug)]
pub struct MapPalette {
    /// Whole-pane land background (the "paper").
    pub land: Color32,
    /// Water-body / waterway / lake fill.
    pub water: Color32,
    /// Park / forest / landuse fill.
    pub green: Color32,
    /// Building footprint fill.
    pub building: Color32,
    /// Coastline stroke (a touch darker than the water fill).
    pub coast: Color32,
    /// Motorway / trunk fill (the top road tier — the warm accent in theme mode).
    pub road_motorway: Color32,
    /// Primary-road fill.
    pub road_primary: Color32,
    /// Secondary-road fill.
    pub road_secondary: Color32,
    /// Tertiary-road fill.
    pub road_tertiary: Color32,
    /// Residential / service / minor-road fill.
    pub road_residential: Color32,
    /// Footway / cycleway / path fill.
    pub path: Color32,
    /// Untagged / generic line fill.
    pub road_generic: Color32,
    /// **The OVERPRINT ink** — geometry from a source that carries no OSM tags AT ALL
    /// (a plain WKB table the operator explicitly pointed the host at: Trafikverket,
    /// NVDB, a survey export). See [`untagged_source_style`].
    ///
    /// It is deliberately NOT a road tier and NOT [`road_generic`](Self::road_generic).
    /// `road_generic` is the *guess* [`has_nothing_to_draw`] exists to stop painting
    /// across open water; reusing it here would put that exact ink back on screen for a
    /// whole file. And a road tier would be a lie: the reader knows the geometry's
    /// SOURCE, never its class.
    ///
    /// Magenta, because that is what an overprint is in cartography — the revision
    /// colour a survey adds on top of a printed base sheet, chosen precisely because no
    /// base-map feature owns it. Nothing else in either preset is in the magenta family,
    /// so an operator can tell at a glance which lines came out of the file they named.
    pub overlay: Color32,
    /// Road **casing** — a dark outline drawn wider than, and under, the road
    /// fill so bright roads read as ribbons on the paper (two-pass stroke).
    pub casing: Color32,
}

/// Which palette a map surface paints with. `Theme` derives every colour off the
/// active [`Theme`] (the re-themeable look); the `Cartographic*` variants select
/// the fixed concrete-hex [`MapPalette::cartographic`] preset (day = light paper,
/// night = dark skade basemap) *alongside* the themed one. Shared by both 2D
/// surfaces so the selector reads + serialises identically (REUSE).
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum PaletteMode {
    /// Colours derived from the active theme (the historical, re-themeable look).
    #[default]
    Theme,
    /// The light-paper cartographic preset.
    CartographicDay,
    /// The dark skade-style cartographic preset.
    CartographicNight,
}

impl PaletteMode {
    /// A stable label for `state_json` / a legend (`"theme"`, `"cartographic-day"`,
    /// `"cartographic-night"`).
    pub fn label(self) -> &'static str {
        match self {
            PaletteMode::Theme => "theme",
            PaletteMode::CartographicDay => "cartographic-day",
            PaletteMode::CartographicNight => "cartographic-night",
        }
    }

    /// `true` for the fixed cartographic presets (not the theme-derived look).
    pub fn is_cartographic(self) -> bool {
        !matches!(self, PaletteMode::Theme)
    }

    /// Resolve to a concrete [`MapPalette`]: the theme-derived palette for
    /// [`Theme`](PaletteMode::Theme), else the matching cartographic preset (the
    /// `theme` argument is ignored for the fixed presets).
    pub fn palette(self, theme: &Theme) -> MapPalette {
        match self {
            PaletteMode::Theme => MapPalette::from_theme(theme),
            PaletteMode::CartographicDay => MapPalette::cartographic(true),
            PaletteMode::CartographicNight => MapPalette::cartographic(false),
        }
    }
}

/// **The OVERPRINT ink, day** — `#B5197A`. See [`MapPalette::overlay`].
///
/// MEASURED (WCAG contrast, [`contrast_ratio`], against the cartographic DAY preset):
/// `4.37` against `road_generic` `#DED8CB`, `5.41` against the paper `#F2EFE9`, `3.79`
/// against the water `#A8D0E6`. The ink it replaces is at `1.24` against that same
/// paper — `road_generic` was never visible on land at all; it only ever showed up
/// because it was being drawn across BLUE, which is the defect lane SI removed.
pub const OVERPRINT_DAY: Color32 = Color32::from_rgb(0xB5, 0x19, 0x7A);

/// **The OVERPRINT ink, night** — `#FF5FC8`. MEASURED: `7.04` against the night
/// ground `#0B0F1A`, `4.34` against the night water `#123A5A`, `3.73` against the
/// night `road_generic` `#3A424F`.
pub const OVERPRINT_NIGHT: Color32 = Color32::from_rgb(0xFF, 0x5F, 0xC8);

/// WCAG relative luminance of an sRGB colour, `0.0..=1.0` — the one place the
/// "is this paper light or dark?" question is answered, so a palette cannot end up
/// with dark ink on dark paper by having two spellings of it.
#[must_use]
pub fn relative_luminance(c: Color32) -> f32 {
    let lin = |v: u8| {
        let s = v as f32 / 255.0;
        if s <= 0.040_45 {
            s / 12.92
        } else {
            ((s + 0.055) / 1.055).powf(2.4)
        }
    };
    0.2126 * lin(c.r()) + 0.7152 * lin(c.g()) + 0.0722 * lin(c.b())
}

/// The WCAG contrast ratio between two sRGB colours, `1.0..=21.0`. Body text wants
/// `>= 4.5`; the map label ink is held well above it.
#[must_use]
pub fn contrast_ratio(a: Color32, b: Color32) -> f32 {
    let (x, y) = (relative_luminance(a), relative_luminance(b));
    let (hi, lo) = if x > y { (x, y) } else { (y, x) };
    (hi + 0.05) / (lo + 0.05)
}

/// Linearly blend two colours (`t=0 → a`, `t=1 → b`).
fn mix(a: Color32, b: Color32, t: f32) -> Color32 {
    let l = |x: u8, y: u8| (x as f32 * (1.0 - t) + y as f32 * t).round().clamp(0.0, 255.0) as u8;
    Color32::from_rgb(l(a.r(), b.r()), l(a.g(), b.g()), l(a.b(), b.b()))
}

impl MapPalette {
    /// Derive the map fill palette from a [`Theme`]. The land background is a
    /// gentle lift off the theme background (so the pane reads as paper, not the
    /// void); water/green/building are tinted toward canonical map hues but kept
    /// in the theme's family via a blend with the accent / point colours.
    pub fn from_theme(theme: &Theme) -> Self {
        // Land: lift the bg toward a neutral grey so areas + roads sit on a
        // readable "paper", but keep it dark enough that a dark theme stays dark.
        let land = mix(theme.bg, Color32::from_rgb(60, 62, 72), 0.30);
        // Water: blend the accent toward a map blue, then darken to a fill (an
        // OSM map's water is a calm mid-blue, not the bright stroke colour).
        let water = mix(mix(theme.accent, Color32::from_rgb(70, 120, 185), 0.6), land, 0.30);
        // Green: blend the point colour toward a muted park/forest green.
        let green = mix(mix(theme.point, Color32::from_rgb(96, 150, 96), 0.6), land, 0.25);
        // Building: a subtle grey just off the land, a touch warmer/lighter.
        let building = mix(land, Color32::from_rgb(120, 118, 124), 0.45);
        // Coastline: darker than the water fill so the shore reads as a line.
        let coast = mix(water, Color32::BLACK, 0.35);
        // Road tiers — the exact colours the theme-mode classifier has always
        // used, hoisted onto the palette so the classifier is a pure colour
        // lookup and a cartographic preset can swap them wholesale.
        let road_motorway = theme.accent;
        let road_primary = mix(theme.accent, theme.text, 0.5);
        let road_secondary = theme.text;
        let road_tertiary = mix(theme.text, theme.text_dim, 0.4);
        let road_residential = theme.text_dim;
        let path = mix(green, theme.text_dim, 0.4);
        let road_generic = theme.text_dim;
        // The overprint ink is NOT theme-derived on purpose: every road tier here is
        // `accent`/`text`-derived, so a derived overlay would collide with one of them
        // under some theme and the "which lines came from my file" question would stop
        // having an answer. A fixed magenta, blended 15 % toward `land` so it sits in
        // the pane's value range without losing its identity.
        let overlay = mix(OVERPRINT_DAY, land, 0.15);
        // Casing: a dark line just off the land (only used when a view opts into
        // casing; theme mode historically drew no casing, so this is inert there
        // until requested).
        let casing = mix(land, Color32::BLACK, 0.55);
        Self {
            land,
            water,
            green,
            building,
            coast,
            road_motorway,
            road_primary,
            road_secondary,
            road_tertiary,
            road_residential,
            path,
            road_generic,
            overlay,
            casing,
        }
    }

    /// A **concrete-hex cartographic preset** — a real map's palette, independent
    /// of the theme. `day = true` is a light paper map (warm land, calm blue
    /// water, an orange→yellow→white road ramp); `day = false` is a dark
    /// skade-style basemap (near-black navy land, cyan water + top roads). Every
    /// colour is a fixed sRGB hex so the map looks identical under any theme —
    /// selected when the host wants the cartographic look rather than the themed
    /// one. Casing is a near-background dark so bright roads read as ribbons.
    pub fn cartographic(day: bool) -> Self {
        if day {
            Self {
                land: Color32::from_rgb(0xF2, 0xEF, 0xE9),
                water: Color32::from_rgb(0xA8, 0xD0, 0xE6),
                green: Color32::from_rgb(0xC8, 0xE6, 0xC9),
                building: Color32::from_rgb(0xE2, 0xDB, 0xCF),
                coast: Color32::from_rgb(0x5A, 0x8F, 0xB0),
                road_motorway: Color32::from_rgb(0xE9, 0xA2, 0x3B),
                road_primary: Color32::from_rgb(0xF6, 0xD3, 0x65),
                road_secondary: Color32::from_rgb(0xFF, 0xFF, 0xFF),
                road_tertiary: Color32::from_rgb(0xFB, 0xFB, 0xF7),
                road_residential: Color32::from_rgb(0xF3, 0xF1, 0xEA),
                path: Color32::from_rgb(0xB9, 0x98, 0x73),
                road_generic: Color32::from_rgb(0xDE, 0xD8, 0xCB),
                overlay: OVERPRINT_DAY,
                casing: Color32::from_rgb(0xB7, 0xAE, 0x9E),
            }
        } else {
            Self {
                land: Color32::from_rgb(0x0B, 0x0F, 0x1A),
                water: Color32::from_rgb(0x12, 0x3A, 0x5A),
                green: Color32::from_rgb(0x14, 0x36, 0x1F),
                building: Color32::from_rgb(0x1A, 0x22, 0x33),
                coast: Color32::from_rgb(0x2E, 0x7F, 0xA6),
                road_motorway: Color32::from_rgb(0x46, 0xC6, 0xE0),
                road_primary: Color32::from_rgb(0x3A, 0xA6, 0xC8),
                road_secondary: Color32::from_rgb(0x8A, 0x9C, 0xB2),
                road_tertiary: Color32::from_rgb(0x6E, 0x7E, 0x92),
                road_residential: Color32::from_rgb(0x55, 0x62, 0x75),
                path: Color32::from_rgb(0x3E, 0x5A, 0x46),
                road_generic: Color32::from_rgb(0x3A, 0x42, 0x4F),
                overlay: OVERPRINT_NIGHT,
                casing: Color32::from_rgb(0x05, 0x07, 0x0C),
            }
        }
    }

    /// **The LABEL ink for this palette** — dark on light paper, light on dark.
    ///
    /// THE DEFECT THIS CLOSES. `MapTheme::cartographic` took its label colour from the
    /// HOST's `facett_core::Theme::text`, with a comment claiming that kept text
    /// "legible against the app's own chrome". The map is not chrome: korp's app theme
    /// is dark, so `theme.text` is `#DCDCDC`, and the cartographic day map printed
    /// `#DCDCDC` on `#F2EFE9` paper. MEASURED on the shipped korp-ui over Stockholm at
    /// zoom 12.7: `#DCDCDC` covered **9 387 px (1.03 %)** of the map canvas at a
    /// contrast ratio of **1.18 : 1** against the land. WCAG asks 4.5 : 1 for body
    /// text. Text at 1.18 : 1 is not read — it is *seen*, as grain. That is the worst
    /// of both outcomes: all of the clutter, none of the information.
    ///
    /// Derived from the palette's own `land` rather than pinned per preset, so a
    /// theme-derived palette (`from_theme`, which can be light or dark) gets a correct
    /// answer by construction instead of by review. MEASURED by
    /// `label_collision::the_day_map_prints_its_labels_at_readable_contrast`: day
    /// `#F2EFE9` land → `#181817` ink at **15.48 : 1**; night `#0B0F1A` land →
    /// `#EBECED` ink at **16.18 : 1**.
    #[must_use]
    pub fn label(&self) -> Color32 {
        if relative_luminance(self.land) > 0.5 {
            mix(self.land, Color32::BLACK, 0.9)
        } else {
            mix(self.land, Color32::WHITE, 0.92)
        }
    }

    /// **The label HALO** — the thin outline drawn around label text so a name stays
    /// readable where it crosses a dark road casing or a water fill, instead of being
    /// swallowed by whatever it happens to lie on. It is the paper itself, which is why
    /// it is invisible over open land and only appears where it is needed.
    #[must_use]
    pub fn label_halo(&self) -> Color32 {
        self.land
    }

    /// **The RAIL ink** — the hue a `railway=*` line is drawn in.
    ///
    /// It exists because `railway=*` had no branch in [`classify_with`] at all: a rail
    /// fell through to the generic arm and painted in `road_generic`, the same ink as an
    /// untagged fence. A national rail network is not an untagged anything, and a look
    /// that cannot tell the two apart cannot be corrected by a tier alone.
    ///
    /// Derived from the palette's own `land` rather than pinned per preset — the same
    /// construction [`label`](Self::label) uses and for the same reason: `from_theme`
    /// can be light or dark, and a hard-coded slate would be invisible on the dark
    /// preset. Day `#F2EFE9` land → `#5C5B59` (contrast 5.91 : 1); night `#0B0F1A`
    /// land → `#919398` (contrast 6.22 : 1). Both well past the 4.5 : 1 a reader needs,
    /// because a rail is a line you follow rather than grain you tolerate.
    ///
    /// **korp keeps its own `map_paint::RAIL_INK` and that is not a twin.** korp inks by
    /// NETWORK identity — the `network` column of `rails.parquet`, known before any tag
    /// is read — and can therefore ink a rail whose tags it never looked at. This inks by
    /// TAG, which is the only thing available for a railway that arrives inside a
    /// monolith/basemap tile carrying no network id. Two different questions, two
    /// answers; the tier below is what they now agree about.
    ///
    /// # This is the CORE of a two-tone line (GG #28)
    ///
    /// A rail is cased like every other opaque line — [`casing`](Self::casing) under it on
    /// both painters — so what this returns is the tone INSIDE that casing, and the two
    /// have to be told apart or the rail is one dark stroke. They were not: on a light
    /// host theme through [`from_theme`](Self::from_theme) the casing is `land` 55 % toward
    /// black and this was `land` 62 % toward black — 17 sRGB units apart, one tone.
    ///
    /// So the core is chosen against BOTH the paper and the casing: of three candidates
    /// anchored on the paper — dark (62 % toward black), light (55 % toward white) and the
    /// midpoint between casing and paper — the one that is farthest from whichever of the
    /// two it is closest to. No threshold to tune, and on every preset the answer is the
    /// one a reader wants:
    ///
    /// * `cartographic(day)` — light casing `#B7AE9E` on `#F2EFE9` paper: the DARK core,
    ///   `#5C5B59` (5.91 : 1 against the paper, unchanged from before);
    /// * `cartographic(night)` / `from_theme` on a dark host — near-black casing on dark
    ///   paper: the LIGHT core (unchanged);
    /// * `from_theme` on a light host — dark casing on light paper: the midpoint, a grey
    ///   the casing frames on both sides. The Garmin reading — dark edges, light body —
    ///   and, since HO-T3, the dashes: this core is cut along the way's arc length at
    ///   [`dash_period_px`] on BOTH lanes (the arc length rides `LineVertex::col.a` under
    ///   the flag in `half_px`'s sign; the CPU painter strokes one `Shape::line` per lit run),
    ///   so the gaps show this casing and the one-road-two-looks defect `kalm-4` closed
    ///   stays closed.
    #[must_use]
    pub fn rail(&self) -> Color32 {
        let dist = |a: Color32, b: Color32| {
            (i32::from(a.r()) - i32::from(b.r()))
                .abs()
                .max((i32::from(a.g()) - i32::from(b.g())).abs())
                .max((i32::from(a.b()) - i32::from(b.b())).abs())
        };
        let candidates = [
            mix(self.land, Color32::BLACK, 0.62),
            mix(self.land, Color32::WHITE, 0.55),
            mix(self.casing, self.land, 0.5),
        ];
        // The candidate whose NEAREST neighbour (paper or casing) is farthest away. The
        // first wins a tie, so the historical dark core is the default.
        let mut best = candidates[0];
        let mut best_sep = i32::MIN;
        for c in candidates {
            let sep = dist(c, self.land).min(dist(c, self.casing));
            if sep > best_sep {
                best = c;
                best_sep = sep;
            }
        }
        best
    }

    /// The **building OUTLINE stroke** — the hue a building footprint's edge is
    /// drawn in (a shade of the [`building`](Self::building) fill). THE single
    /// source of truth: [`recolor_way`](Self::recolor_way) uses it for every
    /// `WayKind::Building` way, and the 2D viewer's building-vertex (corner) layer
    /// uses it so a corner dot is the same ink as the wall it sits on — rather than
    /// the theme's unrelated `point` hue, which on the cartographic-day paper reads
    /// as a *blue sea* of dots (the reported bug).
    #[must_use]
    pub fn building_stroke(&self) -> Color32 {
        mix(self.building, Color32::BLACK, 0.3)
    }

    /// Recolour a single [`Way`](crate::Way) in place to this palette, keyed on
    /// its [`WayKind`] (+ its width, which encodes the road tier — the `Way`
    /// keeps no raw tags). Lets a host swap an already-built way set from the
    /// theme-derived look to a [`cartographic`](Self::cartographic) preset at
    /// runtime without re-reading the source geometry. Areas keep a fill + a
    /// slightly darker outline; lines take the matching road-tier hue.
    pub fn recolor_way(&self, way: &mut crate::Way) {
        // `stroke` = the line / outline hue; `fill_col` = the area fill hue
        // (`None` for a line class).
        let (stroke, fill_col) = match way.style.kind {
            WayKind::Water => (self.coast, Some(self.water)),
            WayKind::Park => (mix(self.green, Color32::BLACK, 0.2), Some(self.green)),
            WayKind::Building => (self.building_stroke(), Some(self.building)),
            WayKind::Coastline => (self.coast, None),
            WayKind::Path => (self.path, None),
            WayKind::Road => {
                // Width encodes the tier (motorway 2.5 … residential 1.0): pick the hue
                // by the ONE width ladder, `road_rank`, so hue, casing and z-order cannot
                // drift apart. (They did: this was a second copy of the thresholds, and
                // both kept motorway at `>= 2.6` after 7640848 moved motorway to 2.5.)
                let c = match road_rank(way.width) {
                    4 => self.road_motorway,
                    3 => self.road_primary,
                    2 => self.road_secondary,
                    1 => self.road_tertiary,
                    _ => self.road_residential,
                };
                (c, None)
            }
            WayKind::Other => (self.road_generic, None),
            // A railway reskins to THIS palette's rail ink — the ONE writer. While it was
            // `Other` the first `cartographic()` swap put every rail in `road_generic`.
            WayKind::Rail => (self.rail(), None),
            // The overprint keeps its own ink across a palette swap. `Other` would
            // reskin it to `road_generic` — the ink `has_nothing_to_draw` exists to
            // keep off the water — so the whole operator overlay would go back to
            // being an invisible hairline on the first `cartographic()` call.
            WayKind::Overprint => (self.overlay, None),
            // **A GLOW KEEPS ITS OWN INK, exactly as the overprint does.** The trail's
            // colour is the case's, chosen by korp from the neon family; reskinning it
            // here would repaint every trail in one road hue on the first
            // `cartographic()` call and it would stop being a trail at all. This arm is
            // why `Glow` is a KIND and not just a wide `Other` — the same evidence the
            // overprint arm above records, one overlay later.
            WayKind::Glow => (way.style.color, None),
        };
        way.stroke = stroke;
        way.style.color = stroke;
        // Class fill colour rides `style.fill` for an area (so a later relayout
        // still knows the hue); the *painted* `way.fill` recolours only if the
        // geometry was already a filled ring.
        way.style.fill = if way.style.is_area { fill_col } else { None };
        way.fill = way.fill.and(fill_col);
    }
}

/// The **road-class rank** encoded by a road [`Way`](crate::Way)'s width — `4` =
/// motorway/trunk (widest) down to `0` = residential/service (narrowest); paths /
/// non-roads are `0`. The single source of truth for the road hierarchy's
/// **paint z-order** (a car-navigator draws minor roads first and major roads on
/// top, so junctions read as clean continuous major roads) and its **per-class
/// casing width**. Keyed on the same width thresholds [`MapPalette::recolor_way`]
/// uses to pick the tier hue, so colour + width + z-order stay in lockstep (REUSE:
/// no twinned tier table).
pub fn road_rank(width: f32) -> u8 {
    // 2.35 sits between primary's 2.2 and motorway/trunk's 2.5 (7640848 moved motorway
    // from 3.0 to 2.5 — "too fat" on the 890M — and this threshold stayed at 2.6, so a
    // motorway ranked and was hued as a primary).
    if width >= 2.35 {
        4 // motorway / trunk
    } else if width >= 1.9 {
        3 // primary
    } else if width >= 1.5 {
        2 // secondary
    } else if width >= 1.15 {
        1 // tertiary
    } else {
        0 // residential / service / minor
    }
}

// ── Line width: ONE formula, both lanes (the deck.gl `widthMinPixels` lesson) ──
//
// Studied from deck.gl's `PathLayer` vertex shader, which resolves a stroke width as
//
//     vec2 widthPixels = vec2(clamp(
//         project_size_to_pixel(instanceStrokeWidths * path.widthScale, path.widthUnits),
//         path.widthMinPixels, path.widthMaxPixels) / 2.0);
//
// i.e. a world-ish width scaled for the current view and then **clamped into a pixel
// band**, recomputed in the shader every frame. Two properties fall out of that and
// both were missing here:
//
//  1. a road never vanishes when zoomed out (the `widthMinPixels` floor) and never
//     bloats into a slab when zoomed in (the `widthMaxPixels` ceiling); and
//  2. because it is ONE formula, every renderer of the same data agrees on width.
//
// Ours disagreed: the CPU basemap painter scaled `style.width` by a zoom factor in
// `[0.5, 3.0]`, while the GPU line lane baked `style.width × 1.0` **once** into the
// vertex buffer — a constant that could never respond to zoom. The two lanes drew the
// same road 2–3× apart depending on zoom (and the GPU roads stayed frozen while the
// CPU land outlines grew), which reads exactly as "the land and the lines are at two
// different scales". The functions below are that single source of truth; the GPU
// applies them in the vertex shader (per frame, via a uniform) and the CPU painter
// calls them directly.

/// Reference zoom (px per Mercator unit) at which a way's themed `style.width` is
/// drawn at 1× — the midpoint of the scale ramp below.
pub const LINE_WIDTH_ZOOM_REF: f64 = 50_000.0;
/// Lower / upper bound on the zoom-derived width multiplier (the ramp is clamped so a
/// world view keeps hairlines legible and a street view does not explode).
pub const LINE_WIDTH_SCALE_MIN: f32 = 0.5;
pub const LINE_WIDTH_SCALE_MAX: f32 = 3.0;
/// deck.gl's `widthMinPixels` / `widthMaxPixels` — the pixel band every line width is
/// clamped into, so lines stay crisp and bounded at ANY zoom.
pub const LINE_WIDTH_MIN_PX: f32 = 1.0;
pub const LINE_WIDTH_MAX_PX: f32 = 24.0;

/// The zoom-derived width **multiplier** (deck.gl's `widthScale` for the current
/// view). Split out from [`line_width_px`] because the GPU lane passes exactly this
/// one number to the vertex shader as a uniform, which then applies the same clamp —
/// so the baked geometry stays zoom-independent while the drawn width tracks zoom.
#[must_use]
pub fn line_width_scale(zoom: f64) -> f32 {
    ((zoom / LINE_WIDTH_ZOOM_REF) as f32).clamp(LINE_WIDTH_SCALE_MIN, LINE_WIDTH_SCALE_MAX)
}

/// Resolve a themed `style.width` to its **on-screen pixel width** at `zoom` —
/// deck.gl's `clamp(width × widthScale, widthMinPixels, widthMaxPixels)`. The single
/// formula both the CPU painter and the GPU line lane use, so the two never disagree.
#[must_use]
pub fn line_width_px(style_width: f32, zoom: f64) -> f32 {
    line_width_px_scaled(style_width, line_width_scale(zoom))
}

/// [`line_width_px`] with the multiplier supplied directly — the exact arithmetic the
/// line shader performs, so a CPU test can assert the two lanes agree bit-for-bit.
#[must_use]
pub fn line_width_px_scaled(style_width: f32, scale: f32) -> f32 {
    (style_width * scale).clamp(LINE_WIDTH_MIN_PX, LINE_WIDTH_MAX_PX)
}

/// **THE FILL-OUTLINE WIDTH LAW — the stroke on a filled area's rings.**
///
/// `mapgpu-1`. A filled way's outer + hole rings are stroked by both lanes, and the
/// width was written **twice, differently**: `f.width.max(0.5)` in
/// `layer::MapFacet::ui` / `MapView::draw_ways` on the CPU, and an implicit `1.0` on
/// the GPU because `PrimitiveTopology::LineList` has no width at all. So the same
/// ring came out 0.5 px feathered on one lane and 1 px aliased on the other, and the
/// smallest areas lost coverage: MEASURED 2026-08-03 on a real 1600x1000 window,
/// non-background coverage 19 507 px (CPU) -> 17 419 px (GPU), **-10.7 %**.
///
/// This is deliberately NOT [`line_width_px_scaled`]: an area's edge is a hairline
/// that marks a boundary, not a road whose thickness carries meaning, so it does not
/// track zoom and it is allowed below the road band's [`LINE_WIDTH_MIN_PX`] floor.
/// [`FILL_OUTLINE_MIN_PX`] is what the CPU painter's `max(0.5)` always was; naming it
/// is what lets the GPU lane read the same number instead of inventing one.
#[must_use]
pub fn fill_outline_width_px(style_width: f32) -> f32 {
    style_width.max(FILL_OUTLINE_MIN_PX)
}

/// **AA softness (px) on a filled area's ring stroke.**
///
/// Half the road lane's, because the mark is a fraction of a road's width and a feather
/// that is twice the stroke is a smear, not an edge. MEASURED on this box 2026-08-22
/// against the CPU painter it must match (7x7 lakes, 600x600, RTX 4090), as the ratio
/// of GPU ink MASS to CPU ink mass: at the road lane's 1.0 px feather **1.052x**, at
/// 0.5 px **1.040x**. The COUNT of lit pixels moves further than the mass does
/// (1.090x -> 1.030x), which is the tell that what a wider feather adds is ramp WIDTH
/// rather than ink — a pixel-count metric reads a faint tail as a full mark.
pub const FILL_OUTLINE_FEATHER_PX: f32 = 0.5;

/// Floor on a filled area's ring stroke (px) — see [`fill_outline_width_px`].
///
/// It is BELOW [`LINE_WIDTH_MIN_PX`] on purpose. The road band's floor exists so a
/// distant motorway stays legible; a lake's edge has no such duty and a 1 px floor on
/// it would thicken every building footprint in a city viewport.
pub const FILL_OUTLINE_MIN_PX: f32 = 0.5;

/// **THE CASING LAW — the extra half-width (px) a road's dark outline adds per side.**
///
/// `kalm-4`. This arithmetic existed once, in `line.wgsl`, as
/// `min(max(w_px * 0.5 * casing_ratio, casing_min_px), width_add)` — and the CPU
/// painter, which is not a degraded mode but what draws under a bearing, under a tilt,
/// on a host with no adapter and in every `egui_kittest` harness test in this repo,
/// drew **no casing at all**. One road, two looks, and no CPU-side test could see a
/// casing regression because there was no CPU-side casing.
///
/// It is a function and not three constants because the ratio, the floor and the
/// ceiling only mean anything together: `RATIO` alone makes a motorway's casing 1.5 px
/// and a footpath's 0.25 px, and it is `CASING_MIN_PX` that stops the second from
/// vanishing. A second copy of two of the three is how the shader and the painter would
/// drift while both looked right in isolation.
///
/// `width_px` is the **resolved** on-screen width from [`line_width_px_scaled`], not
/// the raw `style.width` — the shader reads it after the same clamp.
#[must_use]
pub fn casing_extra_px(width_px: f32) -> f32 {
    (width_px * 0.5 * CASING_RATIO).clamp(CASING_MIN_PX, CASING_MAX_PX)
}

/// **The casing's extra half-width as a fraction of the way's own half-width.**
///
/// It was a flat `+2 px` for every way regardless of the road's width, so a residential
/// street (1 px core) drew as a 5 px band of near-black with a 1 px light core — four
/// fifths casing — and there are tens of thousands of them in a city viewport. MEASURED
/// before the fix on the shipped korp-ui at zoom 12.7 over Stockholm: the casing colour
/// alone covered 2.81 % of the canvas and 15.67 % of the canvas was ink darker than
/// grey 70; after, 0.83 %. The dark grain the map was accused of was the road casing.
pub const CASING_RATIO: f32 = 0.5;
/// Floor (px) under [`CASING_RATIO`], so the thinnest way keeps a visible edge instead
/// of losing its outline entirely.
pub const CASING_MIN_PX: f32 = 0.35;
/// Ceiling (px) on the casing's extra half-width — this is all a very wide motorway is
/// allowed to grow by.
pub const CASING_MAX_PX: f32 = 2.0;

/// **THE DASH LAW — the period (px) of a dashed way's core, along its ARC LENGTH.**
///
/// HO-T3 (GG #28 found the gap: *"no dash machinery exists"*). A Garmin rail is a dark
/// casing with a light core cut into dashes; the dash is a property of the way's
/// LENGTH, not of the screen, so it must not crawl when the map pans and a diagonal
/// rail must carry exactly as many dashes as a horizontal one of the same length.
///
/// The period follows the way's own resolved width, because the width is what the eye
/// measures a dash against:
///
/// ```text
///     period_px = DASH_PERIOD_RATIO · w_px          (w_px = line_width_px(style.width, zoom))
///     dash      = DASH_DUTY · period_px  lit, the rest shows the casing
/// ```
///
/// One function, both lanes — the CPU painter hands it to `Shape::dashed_line`, and
/// `line.wgsl` carries the two constants as WGSL twins (`DASH_PERIOD_RATIO`,
/// `DASH_DUTY`) that `tests/rail_dash_pixels.rs` reads out of the shader text and
/// asserts equal. `width_px` is the RESOLVED on-screen width from
/// [`line_width_px_scaled`], exactly as [`casing_extra_px`] takes it.
#[must_use]
pub fn dash_period_px(width_px: f32) -> f32 {
    width_px * DASH_PERIOD_RATIO
}

/// Dash period as a multiple of the way's resolved width — see [`dash_period_px`].
/// `4`: a 2 px trunk rail at 1× is 4 px lit / 4 px dark, the Garmin proportion.
pub const DASH_PERIOD_RATIO: f32 = 4.0;
/// The lit fraction of one dash period — see [`dash_period_px`].
pub const DASH_DUTY: f32 = 0.5;

impl WayStyle {
    /// **Does this way's core carry dashes?** — the ONE gate both lanes read (HO-T3).
    ///
    /// Derived from the kind rather than stored, so a way built by any of the
    /// constructors — `classify_with`, `Way::line`, a host's own style literal — answers
    /// the same way for the same kind and no constructor has to remember a flag. Today
    /// exactly [`WayKind::Rail`] dashes; a road is unaffected.
    #[must_use]
    pub fn dashed(&self) -> bool {
        self.kind == WayKind::Rail
    }
}

/// Classify an OSM way by its tags into a themed [`WayStyle`]. Mirrors the
/// branch structure of osm-viewer's `classify`, but the colours are pulled from
/// `theme` (so the basemap follows the palette) and area kinds carry a `fill` +
/// `is_area`. `tags` is the decoded JSON tag map (`{"highway":"motorway", ...}`);
/// an empty map yields the generic (line) style.
pub fn classify<K: AsRef<str>, V: AsRef<str>>(tags: &[(K, V)], theme: &Theme) -> WayStyle {
    classify_with(tags, &MapPalette::from_theme(theme))
}

/// The palette-driven core of [`classify`]: classify an OSM way's `tags` into a
/// [`WayStyle`] whose colours are pulled from an explicit [`MapPalette`]. Passing
/// [`MapPalette::from_theme`] reproduces [`classify`] byte-for-byte (the themed
/// look); passing [`MapPalette::cartographic`] paints the concrete-hex preset.
/// This is the single classification path — the two looks differ only in which
/// palette feeds it (REUSE: no twinned tag-branch logic).
pub fn classify_with<K: AsRef<str>, V: AsRef<str>>(tags: &[(K, V)], pal: &MapPalette) -> WayStyle {
    let get = |k: &str| crate::tag(tags, k);
    let has = |k: &str| crate::has_tag(tags, k);

    // ── A FERRY IS A SEA ROUTE, NOT A TRAIL ────────────────────────────────────────
    //
    // **Why this arm is before the marked-route arm.** The arm below opens with a bare
    // `has("route")`, written for hiking/ski/mtb, and `route=ferry` matches it. Every
    // ferry line in the cut was therefore returned as `trail(Lod::District)` — `pal.path`,
    // the brown track ink, drawn from camera 9 up — so an international ferry was painted
    // across open sea as a long straight brown chord. The owner photographed one over Skåne.
    //
    // MEASURED on the served cut (`~/.korp/work/sweden/ways.parquet`, 2026-09-02,
    // 9 325 142 rows): **876** ways carry `route=ferry` or `ferry=*`, **772 of them at
    // `min_zoom` 9** — the server hands them over at exactly the rung the trail arm drew
    // them at. The longest, way **299847757** *"København (DK) – Oslo (N)"*, spans
    // **2.656° of latitude × 2.037° of longitude** in one straight line across the
    // Kattegat. Six of the ten longest carry `ferry=trunk|primary|secondary|tertiary`,
    // i.e. a HIGHWAY CLASS as the value of the `ferry` key — which is the second half of
    // why it reads as a road to anyone looking at the tags.
    //
    // **What it becomes.** `WayKind::Water` in the coastline ink, a LINE (a ferry route
    // never closes into a filled ring), from `Lod::City`. The tier is the judgement:
    // a ferry link is information when the operator is looking at a harbour, and a
    // four-hundred-kilometre chord over a regional view is the clutter that was reported.
    // Drawing LATER than the server sends is the benign direction (see the marked-route
    // arm below for the same argument); the dangerous direction is a client wanting what
    // the server withholds, and this is not that.
    //
    // **The `ferry`-only shape is gated on `highway`** so this can never demote a road:
    // a `highway=*` way that merely mentions a ferry keeps its road class and falls
    // through untouched. `route=ferry` is unambiguous and needs no gate.
    // osm-katana already knows this — `relgeom.rs` excludes `route=ferry` from the trails
    // extract — but that exclusion lived only on the extract side and was never mirrored
    // into the classifier, so the client re-created the mistake from the raw tags.
    if get("route") == Some("ferry") || (has("ferry") && get("highway").is_none()) {
        return WayStyle {
            color: pal.coast,
            width: 0.7,
            lod: Lod::City,
            kind: WayKind::Water,
            is_area: false,
            fill: None,
        };
    }

    // ── MARKED ROUTES — leder (`railtier-1`, the trail half).
    //
    // # Why this arm is BEFORE `highway` and why it has to be
    //
    // A marked route arrives tagged as the way it runs along: 19 664 of the 21 658 hiking
    // rows in the Swedish cut carry `highway=path`, so the arm below tiers them
    // [`Lod::Detail`] — camera 15, the footpath rung, argued for street furniture in a
    // city. A hiking route is not street furniture. MEASURED on the live thin client at
    // camera 9.33: the `trails` layer was fed **6 719** ways and drew **32**, and the 32
    // were the handful whose tags happened to name an arterial.
    //
    // # Why it reads TAGS and not a class
    //
    // Because the class does not reach here. korp cuts `trails.parquet` with a `class`
    // COLUMN (`hiking` / `cycling` / `ski` / `mtb` / `snowmobile`) and filters its server
    // read on it, but `class` is not on the wire — `MapWay`/`MapPolyline` carry `tags`,
    // `osm_id` and `network`, nothing else — so a client has only the tags. What it does
    // have is enough, MEASURED over all **75 694** rows of
    // `~/.korp/work/sweden/networks/trails.parquet` (2026-08-27 cut), each rule scored
    // against the file's own `class` column:
    //
    // | rule | rows it claims | recall of its class | what it steals |
    // |---|---|---|---|
    // | `piste:type` | 9 529 | **91.1 %** of `ski` | 305 `hiking`, 152 `mtb` |
    // | `snowmobile` | 32 040 | **99.2 %** of `snowmobile` | 713 `hiking`, 139 `ski`, 102 `mtb` |
    // | `route` | 268 | **100 %** of `cycling` (all 18 rows) | 147 `hiking`, 76 `mtb`, 27 `ski` |
    // | `sac_scale` \| `trail_visibility` | 20 437 | **94.4 %** of `hiking` | nothing |
    // | `mtb:scale` | 12 356 | **97.1 %** of `mtb` | nothing |
    //
    // 721 rows (0.95 %) match nothing and fall through to the `highway` arm exactly as
    // before. The rules are ordered by precision, not by class size: `mtb:scale` is on
    // 100 % of `mtb` AND on 25 % of `hiking`, so it must be asked LAST or it takes a
    // quarter of the hiking network with it.
    //
    // # The tiers, and the one that is deliberately not korp's
    //
    // korp's `map_networks::Network::tier_floors` gives `Trails` the floors `[0, 9, 12]`:
    // hiking from camera 0, cycling+ski from 9, mtb+snowmobile from 12. Ski, cycling, mtb
    // and snowmobile land on their floor exactly. **Hiking does not** — it gets
    // [`Lod::District`] (9) rather than [`Lod::Country`] (0), and that is a choice with a
    // reason: 21 658 hiking paths drawn over a whole-Sweden view is the clutter defect
    // `kalm-1` names, reached from the other side, and the 84 %-recall half of the hiking
    // rule (`trail_visibility`) is not precise enough to earn the top tier. Drawing LATER
    // than the server sends is the benign direction — one rung of bytes that arrive before
    // anything paints them; the dangerous direction is a client wanting what the server
    // withholds, and this is not that.
    //
    // The result on the whole file: **30 234 of 75 694 (39.9 %)** trail ways draw at
    // camera 9.33, against the **41.8 %** korp's own ladder serves at that rung
    // (hiking+cycling+ski, 31 630 rows on this cut). The two ladders now agree to within
    // 1.9 percentage points where they used to disagree by 100 %.
    //
    // # A route along an arterial is still an arterial
    //
    // The `highway` gate below is why this arm cannot silently demote a road: a cycling
    // route signposted along a primary road carries the relation's `route=bicycle` onto
    // the way, and letting the route win would drop an arterial from [`Lod::Country`] to
    // [`Lod::District`]. 343 rows in the trails cut are such ways. The route is a property
    // of the road; the road is the object.
    if !matches!(
        get("highway"),
        Some(
            "motorway" | "motorway_link" | "trunk" | "trunk_link" | "primary" | "primary_link"
                | "secondary" | "secondary_link" | "tertiary" | "tertiary_link"
        )
    ) {
        // Ordered by the precision measured above. `is_area: false` throughout: a marked
        // route is a line even when it closes into a loop.
        let trail = |lod| WayStyle {
            color: pal.path,
            width: 0.9,
            lod,
            kind: WayKind::Path,
            is_area: false,
            fill: None,
        };
        if has("piste:type") {
            return trail(Lod::District);
        }
        if has("snowmobile") {
            return trail(Lod::City);
        }
        if has("route") {
            return trail(Lod::District);
        }
        if has("sac_scale") || has("trail_visibility") {
            return trail(Lod::District);
        }
        if has("mtb:scale") {
            return trail(Lod::City);
        }
    }

    // Roads + paths are LINES (strokes), styled by `highway` class. Widths are
    // sensible logical-px at fit-zoom (the painter scales them with zoom); the
    // colours ride the palette so motorways pop and minor roads recede.
    if let Some(hw) = get("highway") {
        let line = |color, width, lod, kind| WayStyle { color, width, lod, kind, is_area: false, fill: None };
        return match hw {
            // **2.5, not 3.0 — the owner's eye on the 890M, 2026-09-06.** With GG #27 both
            // lanes finally read `style::line_width_px`, and at the ramp's 3.0× ceiling
            // (slippy zoom ≳ 9.2) a 3.0 style width lands at **9 px** against `primary`'s
            // **6.6** — *"the orange ones are a bit too fat, the yellow ones are perfect"*.
            // The ramp is NOT the defect (the yellow it produces is the one he approved);
            // the CLASS width was, and motorway/trunk is the only band he called out. 2.5
            // lands at **7.5 px**, so the ladder keeps its order and its gap to `primary`.
            "motorway" | "trunk" => line(pal.road_motorway, 2.5, Lod::Country, WayKind::Road),
            "primary" => line(pal.road_primary, 2.2, Lod::Country, WayKind::Road),
            // ── THE RAMPS ARE NOT THE ROAD (`railtier-2`).
            //
            // `motorway_link` / `trunk_link` / `primary_link` shared `Lod::Country` with the
            // roads they ramp off, and korp does not send any of them until camera **7**
            // (`map_networks::Network::tiers`, the z7-8 rung: *"+ the links that make the
            // skeleton connected"* — its z<=6 tier is `motorway`, `trunk`, `primary` and
            // nothing else). So for SEVEN zooms the tier claimed a visibility the wire
            // could not deliver, and korp is the side that is right: an interchange ramp is
            // not a country-scale object, and at camera 6 one painted at the motorway's
            // 3.0 width is a blob competing with the trunk line it belongs to.
            //
            // `Lod::Region`, camera 6, is the closest rung the five-tier ladder has to
            // korp's 7 — the residual one-zoom gap is the granularity artefact named in
            // korp's `tests/net_ladder_vs_draw_tier.rs`. **This changes no pixel**, and that
            // is now MEASURED there rather than argued here, on two facts taken from the
            // live 2026-08-27 Sweden cut:
            //
            // * the band `6.0 <= camera < 7.0` floors onto the SAME tile-pyramid rung as
            //   camera 6 (`[6, 9, 12, 15]`), so it reads the very cut the class ladder has
            //   already filtered — asserted, so a future residue that straddles a rung is
            //   red;
            // * the one reader with NO class filter — the basemap tile read — carries none
            //   of them: 19 z6 tiles, 493 072 rows, 80 331 non-point geometries, **64** with
            //   any `highway` tag (46 construction, 17 proposed, 1 abandoned) and **0** of
            //   these four. Roads live in `networks/roads.parquet`, where the ladder governs.
            //
            // What it changes is the promise. Also refused, deliberately: a sixth `Lod` at
            // camera 7. Camera 7 is korp's BUDGET rung, not a cartographic one, and putting
            // a `LOD_TIER_ZOOMS` step where nothing about the LOOK changes would move every
            // `Lod as u32` index in this file, `cull.wgsl` and `lod_ladder_vs_pyramid.rs` to
            // encode a row count.
            //
            // Ink and width stay the road's: a ramp off a motorway IS motorway asphalt,
            // and inventing a third colour here would be an unargued taste call.
            "motorway_link" | "trunk_link" => {
                line(pal.road_motorway, 3.0, Lod::Region, WayKind::Road)
            }
            "primary_link" => line(pal.road_primary, 2.2, Lod::Region, WayKind::Road),
            "secondary" => line(pal.road_secondary, 1.7, Lod::Region, WayKind::Road),
            // `secondary_link` splits off `secondary` for the same reason and lands EXACTLY
            // on korp's floor: korp sends `secondary` from 7 but `secondary_link` from 9,
            // in the same rung as `tertiary`. Painting it from 6 was a three-zoom promise
            // gap — the widest of the six this pass found — and `Lod::District` closes it
            // to zero rather than merely narrowing it.
            "secondary_link" => line(pal.road_secondary, 1.7, Lod::District, WayKind::Road),
            // `Lod::District`, not `Lod::Region` — the z9 rung. A tertiary road is the
            // sub-regional network: too fine to read on a country overview, too coarse
            // to wait for the city tier. It shared `Region` with `secondary` only
            // because there was no rung between 6 and 12 to put it on.
            "tertiary" | "tertiary_link" => line(pal.road_tertiary, 1.3, Lod::District, WayKind::Road),
            "residential" | "unclassified" | "living_street" => {
                line(pal.road_residential, 1.0, Lod::City, WayKind::Road)
            }
            // ── `service` JOINS `track` ON THE DETAIL RUNG (`railtier-2`).
            //
            // It sat with `residential` at `Lod::City`, camera 12, while korp holds it —
            // together with `track` — to its own z14 rung, *"the minor VEHICULAR tail, and
            // nothing pedestrian"*. Two zooms in which the tier said driveways and parking
            // aisles were eligible and the wire carried none: 599 416 rows, the biggest
            // single class in `roads.parquet`, promised and absent.
            //
            // `track` — korp's OTHER member of that same rung — has been `Lod::Detail`
            // since `kalm-1`, and the arm below says in so many words that it is styled AS
            // `service` because korp treats them as the same class of thing. They were
            // identical in ink, width and kind and differed only in tier, which is the
            // inconsistency rather than the design. Now they are the same in all four.
            //
            // **This changes no pixel either**: korp sends neither before 14, so nothing
            // was drawn at 12-13 to lose. It turns a dangerous-direction gap (the painter
            // wanting what the wire withholds) into the benign one the `track` arm already
            // argues for: one zoom of bytes that arrive before anything paints them.
            "service" => line(pal.road_residential, 1.0, Lod::Detail, WayKind::Road),
            // **A FOREST ROAD YOU CAN DRIVE DOWN IS NOT A FOOTPATH.** `track` used to sit
            // in the footpath arm below, painted in `pal.path` at 0.7 and classified
            // `WayKind::Path`. That arm justifies itself with `kalm-1`, a RENDERING-
            // DENSITY argument about sub-pixel footpaths; `track` was swept in with them
            // and never argued for on its own. It is a vehicular road by OSM definition
            // — agricultural and forestry roads you can drive — and korp's histogram of
            // the Sweden extract names it exactly that ("`track` (316 960 — forest and
            // field roads)"), filing it with `service` in the minor VEHICULAR tail.
            //
            // Styled AS `service`, deliberately: korp treats them as the same class of
            // thing, and any width or ink difference invented here would be the same
            // unargued taste call that created the defect.
            //
            // **THE TIER DOES NOT MOVE — `Lod::Detail`, camera 15 — and this is the half
            // a future reader will get wrong.** korp sends tracks from z14. The client
            // drawing LATER than the server sends is the benign direction: one zoom of
            // bytes that arrive before anything paints them. The dangerous direction is
            // the opposite, and this is not it. The two ladders have different
            // granularity BY DESIGN (five cartographic tiers against six wire floors), so
            // "helpfully" aligning this to 14 would introduce a mismatch across two
            // ladders that are meant to be independent. See
            // `tests::a_track_is_a_vehicular_road_not_a_footpath`.
            "track" => line(pal.road_residential, 1.0, Lod::Detail, WayKind::Road),
            // `Lod::Detail`, not `Lod::City` — `kalm-1`. A footpath is the finest thing
            // the extract carries and it was tiered with the residential street, so the
            // entire footpath network of a city drew at zoom 12, where it is sub-pixel.
            "footway" | "cycleway" | "path" | "pedestrian" | "steps" => {
                line(pal.path, 0.7, Lod::Detail, WayKind::Path)
            }
            // ── THE ROSTER RECONCILIATION. korp enumerates 22 `highway` classes and
            // admits each from a floor LOD; this file enumerated 20, and the six it did
            // not name fell through to `_`. A fallback is not a tier policy, it is the
            // absence of one, and it landed three of them three zooms late.
            //
            // `road` is OSM's "class not surveyed"; `busway` and `bus_guideway` are
            // bus-only carriageways. korp files all three in its z9-11 tier, the county
            // road net. **Only expressible since `98e4a316`** — `Lod::District` is the
            // z9 rung and did not exist before it.
            //
            // The INK stays `road_generic` deliberately. The defect is the tier; giving
            // an unsurveyed class or a bus-only carriageway a tertiary road's colour
            // would assert a classification nobody has made, which is the same unargued
            // taste call that put `track` in the footpath arm.
            "road" | "busway" | "bus_guideway" => {
                line(pal.road_generic, 0.9, Lod::District, WayKind::Road)
            }
            // korp's z12-13 tier. These already landed on `Lod::City` — but only because
            // the fallback happened to point at the right tier, and correct by accident
            // is not correct: nothing recorded that it had to stay there. Naming them
            // changes no pixel today and makes `_` mean what it says.
            "raceway" | "rest_area" | "services" => {
                line(pal.road_generic, 0.9, Lod::City, WayKind::Road)
            }
            // Reached only by a value NEITHER roster names. korp admits those at its
            // z >= 15 escape hatch (`&[]`, no filter), so nothing arrives before 15
            // whatever this says. See `tests::every_class_korp_sends_is_drawn_at_the_
            // zoom_korp_sends_it`.
            _ => line(pal.road_generic, 0.9, Lod::City, WayKind::Road),
        };
    }

    // ── RAILWAYS — the class that had NO branch here at all (`railtier-1`).
    //
    // # What it cost, measured on the owner's own run
    //
    // `railway=*` matched nothing above and fell through to the generic arm at the
    // bottom of this function, which stamps [`Lod::City`] — a tier `LOD_TIER_ZOOMS`
    // does not enter until camera **12**. `layer::lod_visible` is `lod <= view_lod_tier`,
    // so EVERY rail way was culled at EVERY camera below 12, whatever ink it carried.
    //
    // MEASURED 2026-08-27 on the live thin client (`korp BUILD 7a8733e91f` against oden
    // `:50051`), `$KORP_STATE.map.scene.applied.layers` at camera zoom **9.33** (tier
    // `District`): the `rails` layer was fed **7 217** ways and drew **0**. Not "drew
    // faintly" — zero, at the zoom a national rail network is exactly the thing you are
    // looking for. The fed count is what made it invisible for so long: every counter on
    // the pane read healthy.
    //
    // # Why THESE three tiers
    //
    // They are not a taste call. korp's `map_networks::Network::tier_floors` already
    // gives `Rails` the floors `[0, 9, 12]` and enumerates which classes ride each rung,
    // and those floors are EXACTLY `LOD_TIER_ZOOMS[Country] = 0`,
    // `[District] = 9`, `[City] = 12`. The server was already shipping the national net
    // from camera 0 while this file threw 100 % of it away until 12; the two ladders
    // disagreed, and the honest fix is for the painter to draw the rung the server
    // sends rather than to invent a fourth policy.
    //
    // * `rail` / `narrow_gauge` — 82.3 % of the Swedish rail file and the whole extent of
    //   the network. [`Lod::Country`], beside the motorway, because that is what it is:
    //   the E4 and the Västra stambanan are the same class of object at the same zoom.
    // * urban rail (`light_rail`, `subway`, `tram`, `funicular`, `preserved`,
    //   `miniature`) — [`Lod::District`], the z9 rung. A tram network is a city-region
    //   fact, legible once the camera is over a city and meaningless over a country.
    // * everything else — platforms, sidings, yards, turntables, signal boxes, and every
    //   `railway=` value nobody enumerated — [`Lod::City`], camera 12. This arm is also
    //   where `abandoned` / `disused` / `razed` / `construction` land, which is the right
    //   depth for them: they are ground truth about the ground, not the network.
    //
    // # Position in this function, which is load-bearing
    //
    // AFTER the `highway` arm on purpose. A `highway=footway` carrying `railway=crossing`
    // is a footpath that crosses a railway, not a railway; letting rail win there would
    // reclassify level crossings into the rail network and move a footway three tiers up.
    // Rails proper carry no `highway` tag — VERIFIED on
    // `~/.korp/work/sweden/networks/rails.parquet` (29 683 rows, 2026-08-27 cut): every
    // sampled row carries `railway=`, none carries `highway=`.
    //
    // BEFORE the `landuse` arm, which is the other trap: `has("landuse")` is an
    // unqualified catch-all that paints anything carrying the key as a green park, and
    // `landuse=railway` is a real and common tag on rail corridors.
    //
    // # The KIND (GG #28, *"rail has no kind"*)
    //
    // The two network rungs are `WayKind::Rail`; the third rung stays `WayKind::Other`.
    // `Rail` is what puts a railway INSIDE the road ladder (`paint_order::RAIL_CLASS`:
    // trunk rail between primary and secondary, urban rail between tertiary and
    // residential) instead of at `Other`'s overlay offset above every road, and what lets
    // `recolor_way` keep it in the rail ink across a palette swap. A platform, a disused
    // bed, a construction site or a yard turntable is NOT the network — it is ground truth
    // about the ground — so it keeps the catch-all kind and paints where it always did.
    //
    // # The WIDTHS, which are the casing
    //
    // A rail is two-tone by casing + core (and dashed along that core since HO-T3 — see
    // `WayStyle::dashed` and `dash_period_px`), and
    // the casing law (`casing_extra_px`) is proportional: at 1.5 px the casing was
    // 0.375 px a side, under the antialias feather — no second tone to see. Trunk rail
    // is 2.0 (between `secondary` 1.7 and `primary` 2.2, where it also ranks) and urban
    // rail 1.2 (between `residential` 1.0 and `tertiary` 1.3, likewise).
    if let Some(rw) = get("railway") {
        let rline = |width, lod, kind| WayStyle { color: pal.rail(), width, lod, kind, is_area: false, fill: None };
        return match rw {
            "rail" | "narrow_gauge" => rline(2.0, Lod::Country, WayKind::Rail),
            "light_rail" | "subway" | "tram" | "funicular" | "preserved" | "miniature" => {
                rline(1.2, Lod::District, WayKind::Rail)
            }
            _ => rline(0.8, Lod::City, WayKind::Other),
        };
    }

    // Coastline is a LINE (the shore), distinct + a touch heavier.
    if get("natural") == Some("coastline") {
        return WayStyle { color: pal.coast, width: 1.6, lod: Lod::Country, kind: WayKind::Coastline, is_area: false, fill: None };
    }

    let area = |color: Color32, fill: Color32, width, lod, kind| WayStyle {
        color,
        width,
        lod,
        kind,
        is_area: true,
        fill: Some(fill),
    };

    // Water AREAS: natural=water/lake/bay, any waterway polygon → blue fill.
    if matches!(get("natural"), Some("water") | Some("lake") | Some("bay") | Some("wetland")) || has("water") {
        return area(pal.coast, pal.water, 0.8, Lod::Region, WayKind::Water);
    }
    // ── WATERWAYS, tiered by VALUE — the same ladder the `highway` classes above get.
    //
    // THE DEFECT THIS CLOSES. This was ONE branch, `if has("waterway")`, and it gave a
    // river, a stream, a ditch, a drain, a canal, a weir and a dam the identical style:
    // width 1.0, one blue, `is_area: true`, and `Lod::Region`. `Lod::Region` starts at
    // camera **6** ([`crate::layer::LOD_TIER_ZOOMS`] = `[0, 6, 12, 15]`), so every
    // agricultural ditch and field drain in the extract was drawn from a regional
    // overview outwards — at a zoom where a 1 m ditch is far under a pixel. That is the
    // blue hairball the owner reported alongside the paint-order tangle: not too much
    // water, water with no hierarchy. Meanwhile the roads directly above have had a
    // four-rung ladder all along (motorway → Country, secondary → Region, residential →
    // City, footway → Detail, `kalm-1`), so the fix is to stop being the one class that
    // opted out of it.
    //
    // `is_area` is now true only for the values that ARE polygons (`riverbank`, `dock`,
    // `basin`). It rode on every waterway before, including the linear ones. The
    // *painted* fill was already safe — `Way::from_lonlat_styled` drops the fill unless
    // the ring is closed, and both render lanes key on the resolved `Way::fill`
    // (`features_into`'s `let filled = w.fill.is_some()`, `MapFacet::gpu_fillable`), so
    // no stream was ever filled. But `MapPalette::retheme_way` writes `style.fill` from
    // `is_area` alone, and `paint_order` asks "is this an area?" per feature; a linear
    // way claiming to be an area is a lie waiting for its second reader.
    if let Some(ww) = get("waterway") {
        let line_blue = mix(pal.water, Color32::WHITE, 0.1);
        let wline = |width, lod| WayStyle {
            color: line_blue,
            width,
            lod,
            kind: WayKind::Water,
            is_area: false,
            fill: None,
        };
        return match ww {
            // A river is a country-scale feature — the Klarälven belongs on a national
            // overview exactly as the E4 does.
            "river" => wline(2.0, Lod::Country),
            // The mapped POLYGON of a river/dock/basin: an area, and a big one.
            "riverbank" | "dock" | "basin" => {
                area(pal.coast, pal.water, 0.8, Lod::Country, WayKind::Water)
            }
            // A canal is an infrastructure watercourse at the scale a tertiary road is
            // an infrastructure road — the same z9 rung, for the same reason. The river
            // above it (Country) and the stream below it (City) do not move.
            "canal" => wline(1.4, Lod::District),
            "stream" => wline(0.9, Lod::City),
            // The finest rung, with the footpaths: a ditch or a field drain is the
            // finest thing the extract carries and there are hundreds of thousands.
            "ditch" | "drain" => wline(0.6, Lod::Detail),
            // Structures ON the water, not the water: they are point-like at any zoom a
            // human would look at them from.
            "dam" | "weir" | "lock_gate" | "fish_pass" | "waterfall" | "rapids" => wline(0.8, Lod::City),
            _ => wline(0.8, Lod::City),
        };
    }

    // Green AREAS: forest/wood, parks, generic landuse, leisure greens → green fill.
    if matches!(get("natural"), Some("wood") | Some("forest") | Some("scrub") | Some("grassland") | Some("heath")) {
        return area(mix(pal.green, Color32::BLACK, 0.2), pal.green, 0.6, Lod::Region, WayKind::Park);
    }
    if matches!(get("leisure"), Some("park") | Some("garden") | Some("nature_reserve") | Some("recreation_ground")) {
        return area(mix(pal.green, Color32::BLACK, 0.2), pal.green, 0.6, Lod::Region, WayKind::Park);
    }
    if matches!(
        get("landuse"),
        Some("forest") | Some("grass") | Some("meadow") | Some("recreation_ground") | Some("village_green")
            | Some("farmland") | Some("orchard") | Some("vineyard") | Some("cemetery") | Some("allotments")
    ) || has("landuse")
    {
        return area(mix(pal.green, Color32::BLACK, 0.2), pal.green, 0.5, Lod::Region, WayKind::Park);
    }

    // ── A PIER IS GROUND, A PIPELINE IS NOT (lane TL, 2026-09-03) ─────────────────
    //
    // The third of lane SI's three named-but-undecided classes, and the only one of the
    // three that earns ink. `man_made` is a key whose values split cleanly in two:
    //
    //   * things you can STAND ON, which a harbour map is largely made of — `pier`,
    //     `breakwater`, `groyne`, `embankment`, `causeway`. They are land pushed out
    //     into water, and that is exactly how they are drawn: in [`MapPalette::land`],
    //     the paper ink, so a pier reads as a cream finger off the quay and is
    //     invisible where it crosses actual land, which is correct — a pier over land
    //     is not information.
    //   * things that are NOT on the surface at all — `pipeline`, `cable`. Two of the
    //     five sea-crossing chords lane SI measured through Riddarfjärden were exactly
    //     these. They keep drawing NOTHING.
    //
    // **Why this needed [`CLASSIFIED_PAIRS`] and could not just add `man_made` to
    // [`CLASSIFIED_KEYS`].** The key roster admits a way on its KEY alone, and an
    // admitted way whose VALUE matches no arm falls to the generic hairline at the
    // bottom of this function. Putting `man_made` in the key roster would therefore
    // have put `man_made=pipeline` straight back on the water in `road_generic` —
    // re-opening the very defect, through the door built to close it. The pair roster
    // admits `man_made=pier` and leaves `man_made=pipeline` refused.
    if matches!(
        get("man_made"),
        Some("pier") | Some("breakwater") | Some("groyne") | Some("embankment") | Some("causeway")
    ) {
        // `Lod::City`: a pier is a metres-wide structure and is noise at any camera
        // from which a whole harbour fits. `WayKind::Path` because that is what it is
        // to anyone using it, and it keeps piers out of the road-network re-ink korp
        // runs over `WayKind::Road`.
        return WayStyle { color: pal.land, width: 1.2, lod: Lod::City, kind: WayKind::Path, is_area: false, fill: None };
    }

    // Building AREAS → subtle grey fill.
    if has("building") || get("amenity").is_some() {
        return area(mix(pal.building, Color32::BLACK, 0.3), pal.building, 0.5, Lod::City, WayKind::Building);
    }

    // Generic / untagged way → a faint thin line (no fill).
    //
    // **This arm is a GUESS, not a classification** — see [`CLASSIFIED_KEYS`] and
    // [`has_nothing_to_draw`]. A way whose tags name none of the keys the arms above
    // read never reaches a painter: the classifying `Way` constructors refuse it, so
    // this style is what a caller that *asked for a style anyway* gets, and what a way
    // carrying a read key but an unmatched VALUE (`man_made=pier`, `place=islet`)
    // still paints in.
    WayStyle { color: pal.road_generic, width: 0.6, lod: Lod::City, kind: WayKind::Other, is_area: false, fill: None }
}

/// **Every tag key [`classify_with`] actually reads** — the allowlist behind
/// [`has_nothing_to_draw`], and the reason it is a const and not a comment.
///
/// Derived from the function body, and held to it by
/// `tests::the_classified_key_roster_is_the_keys_classify_with_reads`, which scans this
/// file's own text for `get("…")` / `has("…")` inside `classify_with` and refuses any
/// literal missing here. So the roster cannot drift from the arms: adding a branch that
/// reads a new key fails the guard until the key is named here.
pub const CLASSIFIED_KEYS: [&str; 16] = [
    "amenity",
    "building",
    "ferry",
    "highway",
    "landuse",
    "leisure",
    "mtb:scale",
    "natural",
    "piste:type",
    "railway",
    "route",
    "sac_scale",
    "snowmobile",
    "trail_visibility",
    "water",
    "waterway",
];

/// **Do these tags say NOTHING a map can draw?** True when not one key is in
/// [`CLASSIFIED_KEYS`] — i.e. every arm of [`classify_with`] would fall through to the
/// generic hairline at the bottom, which is a guess and not a classification.
///
/// # The defect this closes (lane SI, 2026-09-03)
///
/// The v2.1 sweep's **E sea-ink** oracle reads `RED(2275…4405)` at deep Stockholm
/// (z13–z16) on the live cut. MEASURED on the served cut
/// (`~/.korp/work/sweden/ways.parquet`) over the z14 Stockholm viewport: **16 565** ways
/// intersect it and **496 of them carry NO TAGS AT ALL** — the member geometry of
/// boundary and multipolygon relations, which OSM leaves untagged on purpose. Another
/// few hundred carry only metadata (`source=historical`, `source_ref=ekonomiska kartan`,
/// `maritime=yes`, `seamark:type=cable_submarine`). Every one of them fell to the generic
/// arm, was stroked `road_generic` **#DED8CB** at `Lod::City`, and — because the land
/// silhouette now washes the pane with `theme.sea` — was painted as a long straight
/// **road-coloured chord across open water**. 31 of them cross the open water of that one
/// frame; **90 %** of the oracle's own foreign-ink pixels lie within 3 px of one.
///
/// It is the same shape as the `route=ferry` defect the arm at the top of
/// [`classify_with`] closes, from the other end: there the tags said "ferry" and the
/// classifier read "trail"; here the tags say nothing and the classifier answers "road".
///
/// **The rung is not the bug.** `osm_katana::shared::class::UNCLASSIFIED_MIN_ZOOM` is
/// 12 *because* this arm is `Lod::City`, deliberately, so the two ladders agree; the
/// server hands the geometry over and the client is right to receive it. What is wrong
/// is painting it as a road. Refusing it here needs no re-cut.
///
/// **Why an allowlist and not "has no tags".** MEASURED on the same frame: dropping only
/// the zero-tag ways leaves 82–95 % of the sea ink standing, because half the chords
/// carry a `source` or a `maritime` key and nothing else. The allowlist is not a taste
/// call either — it is [`classify_with`]'s own reading list, guarded against drift.
/// **The VALUE-selective half of the allowlist** — `(key, value)` pairs that
/// [`classify_with`] draws where the KEY alone is not enough to admit a way.
///
/// [`CLASSIFIED_KEYS`] is a key roster, and a key roster can only say "any value under
/// this key draws". For `man_made` that is false in the dangerous direction:
/// `man_made=pier` is ground and `man_made=pipeline` is one of the submarine chords
/// lane SI measured across Riddarfjärden. Admitting the KEY would have sent
/// `man_made=pipeline` to the generic `road_generic` hairline at the bottom of
/// [`classify_with`] — the defect re-opened by the fix.
///
/// So a pair-level roster, held to the function's own text by the same source scan that
/// guards [`CLASSIFIED_KEYS`]: a key read by a VALUE-selective arm must be named here
/// pair by pair, and may not appear in the key roster.
pub const CLASSIFIED_PAIRS: [(&str, &str); 5] = [
    ("man_made", "breakwater"),
    ("man_made", "causeway"),
    ("man_made", "embankment"),
    ("man_made", "groyne"),
    ("man_made", "pier"),
];

/// **EVERY OSM TAG KEY ANY PAINTER IN THIS CRATE READS** — the roster a *server* needs
/// in order to stop shipping tag text no client will ever look at (korp lane GG #19
/// row 2).
///
/// # What this is, and how it differs from [`CLASSIFIED_KEYS`]
///
/// [`CLASSIFIED_KEYS`] answers *"would [`classify_with`] paint this way at all?"* — it
/// is a refusal rule, and it names only the keys that DECIDE a class. This roster is
/// wider on purpose: it is the union of every key that changes ANY painted output, so a
/// filter built from it cannot alter a pixel. Beyond the classifier that is
///
/// * [`classify_label`] — `name`, `place`, `ref`, `highway`;
/// * [`crate::LayerCategory::from_osm_tags`] — `amenity`, `shop`, `building` (the POI
///   buckets);
/// * [`crate::paint_order::parse_osm_z`] — `layer`, `bridge`, `tunnel`, which decide the
///   paint RANK whenever a server did not send one (korp GG #17's `has_rank == false`
///   fallback, and every local file read);
/// * the building extras on [`crate::Way`] — `height`, `building:levels`, `roof:levels`,
///   `roof:shape`, `roof:material`, and the two spellings each of `building:colour` and
///   `roof:colour`;
/// * the road extras — `name`, `maxspeed`, `oneway`, `junction`.
///
/// # Why it is a const with a guard and not a comment
///
/// A server filtering its wire against a STALE copy of this list does not fail loudly:
/// it silently stops sending a key, and a building loses its height or a motorway its
/// shield, on some cameras, for some features. So the roster is held to the source by
/// `tests::the_painted_key_roster_is_every_key_this_crate_reads`, which scans this
/// crate's own text for tag reads — `tag(tags, "…")`, `has_tag(tags, "…")`, and
/// `get("…")` / `has("…")` through the tag closures — and refuses in BOTH directions: a
/// key read but not listed, and a key listed but never read.
///
/// # It is a floor, not a ceiling
///
/// A consumer of this crate may read keys of its own — korp's map SENTINEL rides on
/// `korp:sentinel`, which exists in no OSM schema and which nothing here has heard of.
/// A filter must union this roster with its own, never take it as the whole truth.
pub const PAINTED_KEYS: [&str; 36] = [
    "amenity",
    "bridge",
    "building",
    "building:color",
    "building:colour",
    "building:levels",
    "ferry",
    "height",
    "highway",
    "junction",
    "landuse",
    "layer",
    "leisure",
    "man_made",
    "maxspeed",
    "mtb:scale",
    "name",
    "natural",
    "oneway",
    "piste:type",
    "place",
    "railway",
    "ref",
    "roof:color",
    "roof:colour",
    "roof:levels",
    "roof:material",
    "roof:shape",
    "route",
    "sac_scale",
    "shop",
    "snowmobile",
    "trail_visibility",
    "tunnel",
    "water",
    "waterway",
];

/// Does any painter in this crate read this tag key? See [`PAINTED_KEYS`].
#[must_use]
pub fn is_painted_key(key: &str) -> bool {
    PAINTED_KEYS.contains(&key)
}

/// **Do these tags say NOTHING a map can draw?** True when not one key is in
/// [`CLASSIFIED_KEYS`] and not one `(key, value)` is in [`CLASSIFIED_PAIRS`].
///
/// The pair half only ever ADMITS: a way refused before this const existed is refused
/// still unless it carries one of the five exact pairs named there.
#[must_use]
pub fn has_nothing_to_draw<K: AsRef<str>, V: AsRef<str>>(tags: &[(K, V)]) -> bool {
    !tags.iter().any(|(k, v)| {
        CLASSIFIED_KEYS.contains(&k.as_ref())
            || CLASSIFIED_PAIRS.contains(&(k.as_ref(), v.as_ref()))
    })
}

/// **What a geometry source that carries NO OSM TAGS AT ALL draws in.**
///
/// # The distinction this draws (lane TL, 2026-09-03)
///
/// [`has_nothing_to_draw`] is right and is not weakened by this. But it answers a
/// question about ONE WAY's tags, and there are two different things that look the same
/// from inside it:
///
/// | | what it is | verdict |
/// |---|---|---|
/// | an untagged way inside a TAGGED OSM cut | a boundary relation's member geometry — OSM leaves it untagged on purpose | **draw nothing.** Lane SI, unchanged. |
/// | every way in a source with NO `tags` COLUMN | a plain WKB table an operator explicitly pointed the host at — Trafikverket, NVDB, a survey export | **draw.** It is not OSM and was never going to have OSM tags. |
///
/// The seam is the parquet SCHEMA, not the row: `facett_osm::read_ways_with` already
/// computes `tags_col: Option<&StringArray>` once per batch, and `None` there means the
/// file has no readable `tags` column at ALL. Lane SI left this exact door open on
/// purpose — the tag gate sits on the four CLASSIFYING `Way` constructors and never on
/// `from_lonlat_styled`, because "a caller with its own [`WayStyle`] has already
/// decided". A tagless SOURCE is such a caller.
///
/// # The ink, and why it is not a road tier
///
/// [`MapPalette::overlay`] — magenta, the cartographic overprint colour (see that
/// field). Explicitly NOT `road_generic`: that is the ink lane SI just took off the
/// water, and putting a whole national roads file back into it would undo the fix at
/// scale. Explicitly not a road TIER either — the reader knows this geometry's SOURCE
/// and nothing about its class, and painting an unknown class as `motorway` is the same
/// species of guess the generic arm was.
///
/// # The rung
///
/// [`Lod::Region`] and not [`Lod::City`]. The failure being fixed is INVISIBILITY, and a
/// national roads overlay that first appears at camera 12 is invisible at every camera
/// an operator would load one for. Not [`Lod::Country`] either: that rung carries the
/// classified trunk skeleton, and a whole untagged table smeared over it at continent
/// zoom is the clutter the ferry arm above was written to stop.
///
/// Width `1.6` — the `secondary` weight, twice `road_generic`'s `0.6`. A layer the
/// operator asked for by name reads as a layer, not as a hairline.
#[must_use]
pub fn untagged_source_style(pal: &MapPalette) -> WayStyle {
    WayStyle { color: pal.overlay, width: 1.6, lod: Lod::Region, kind: WayKind::Overprint, is_area: false, fill: None }
}

/// **How far outside the pane a way's CENTRELINE can sit and still paint into it**, in
/// logical px (stinky `seam-15`).
///
/// The cull compares a way's Mercator *envelope* against the viewport, and that envelope
/// is the centreline's — it is never dilated by anything the painter actually draws. A
/// motorway whose centreline misses the pane by one pixel loses its whole 24 px ribbon,
/// its casing and its antialias feather, so the road ends in mid-air at the edge of the
/// canvas and the next pan snaps it back.
///
/// The pad is the widest half-stroke this crate can paint: half of
/// [`LINE_WIDTH_MAX_PX`], plus the casing that rides outside it
/// ([`CASING_MAX_PX`]), plus one pixel of feather. It is deliberately the MAXIMUM
/// rather than per-way: one number in one uniform, asked by both lanes, and the cost of
/// being generous is a handful of extra ways just off-screen — while the cost of being
/// exact per way is a second width resolution living in the cull.
pub const CULL_PAD_PX: f32 = LINE_WIDTH_MAX_PX * 0.5 + CASING_MAX_PX + 1.0;

/// [`CULL_PAD_PX`] in Mercator units at this camera — the ONE writer both the CPU cull
/// ([`crate::layer::WaysLayer::draws`]) and the GPU cull (`cull.wgsl`, via
/// `ViewportCull::min`/`max`) widen their viewport by.
///
/// `0.0` for a degenerate camera: a division that went wrong must widen nothing rather
/// than swallow the world.
#[must_use]
pub fn cull_pad_merc(world_px: f64) -> f64 {
    if !world_px.is_finite() || world_px <= 0.0 {
        return 0.0;
    }
    f64::from(CULL_PAD_PX) / world_px
}

/// **The minimum on-screen extent, in logical px, a FILLED way must reach to be worth
/// drawing** (stinky `kalm-3`).
///
/// MEASURED off `$KORP_STATE` on two settled z12 panes over Stockholm: `building_ways`
/// **37 378** and **34 708** against an `osm_ways` cap of **80 000** — 43–47 % of
/// everything the server is allowed to send, and every one of them a polygon the fill
/// lane tessellates. At camera 12.7 the pane's Mercator scale is 1.70 Mpx for the world,
/// so a 20 m house spans **1.67 px**. They are not the dark grain (`#E2DBCF` fill at
/// 12.08 % of the canvas, `#9E9991` outline at 0.72 %) — they are half the budget spent
/// on shapes with no shape.
///
/// The discrete [`Lod`] ladder cannot fix this: there is no tier that means "when it is
/// big enough", and a building's tier is a property of the building while its size on
/// screen is a property of the camera. This gate is continuous, and it is the ONE number
/// both lanes ask for — see [`min_fill_extent_merc`].
///
/// **3 px, not 4.** A 3 px square is the smallest mark that still reads as a mark rather
/// than as noise, and at the Sweden pyramid's z15 rung (0.608 m/px) a 20 m house is
/// ~33 px, so nothing a human is looking AT is ever near the threshold.
pub const MIN_FILL_EXTENT_PX: f32 = 3.0;

/// **The Mercator-unit extent below which a filled way is not drawn at this camera.**
///
/// ONE writer for a rule that has to hold on TWO lanes or the map draws its lakes twice:
/// [`crate::layer::WaysLayer::draws`] applies it on the CPU, and the GPU cull applies
/// the very same number, handed to `cull.wgsl` as a uniform (`ViewportCull::min_fill_extent`)
/// rather than re-derived in WGSL. A shader cannot call a Rust function; it can be given
/// its answer, and that is the closest a cross-language pair gets to one writer.
///
/// `world_px` is [`crate::layer::MapView::world_px`] — the whole world's width in logical
/// pixels at this zoom — so `extent_merc * world_px` is the way's on-screen extent.
///
/// Returns `0.0` for a degenerate camera, i.e. the gate opens rather than closing: a
/// division that went wrong must never blank the map.
#[must_use]
pub fn min_fill_extent_merc(world_px: f64) -> f32 {
    if !world_px.is_finite() || world_px <= 0.0 {
        return 0.0;
    }
    (f64::from(MIN_FILL_EXTENT_PX) / world_px) as f32
}

/// A **map label** resolved from a feature's OSM tags: the display [`text`](Self::text)
/// (a road `ref` shield like `E4`, else a `name`) plus the [`Lod`] tier at which it
/// first appears. The tier is the declutter law — country/region places show from a
/// country overview, towns at a regional zoom, street names + minor places only once
/// zoomed into a city — so labels thin out with zoom instead of stacking into mush.
/// This is the text half of the "so-perfect Garmin map" (the biggest Garmin-feel
/// lever); [`crate::layer::LabelLayer`] turns these into LOD-gated draw features.
#[derive(Clone, Debug, PartialEq)]
pub struct LabelClass {
    /// The label text to paint (a route `ref`, else a place/street `name`).
    pub text: String,
    /// The zoom tier this label first appears at (the declutter gate).
    pub lod: Lod,
    /// **How much this name is worth WITHIN its tier**, `0.0..=1.0` — the second
    /// ranking key the collision placer needs and did not have.
    ///
    /// [`Lod`] has three steps and `view_lod_tier` saturates at `City`, so past zoom 12
    /// *every* street, park, lake and named building arrives at the placer with the
    /// identical priority and the tie breaks to the input index — i.e. to whatever
    /// order the tiles happened to decode in. A motorway and a cul-de-sac then compete
    /// as equals for the same pixels, and on a pane that can only fit ~65 names the
    /// draw is decided by luck. This is the key that makes the ranking mean something.
    ///
    /// See [`label_rank_of_style`] for the road/area scale; place nodes are ranked by
    /// settlement size in [`classify_label`].
    pub rank: f32,
}

/// **How much a labelled feature is worth within its LOD tier**, `0.0..=1.0`.
///
/// Roads and paths rank by [`classify_with`]'s **own width**, deliberately: that table
/// already encodes the highway hierarchy (motorway `3.0` → footway `0.7`), and a second
/// `match hw { "motorway" => … }` here would be a copy free to drift from it (LAW #5).
/// So motorway `3.0` → `0.80`, primary `2.2` → `0.59`, secondary `1.7` → `0.45`,
/// residential `1.0` → `0.27`, footway `0.7` → `0.19`.
///
/// The non-road kinds are ranked flat, because their sizes are not a hierarchy: a lake
/// name is worth more than a residential street and less than an arterial, a park less
/// than a lake, a named building least of all.
#[must_use]
pub fn label_rank_of_style(s: &WayStyle) -> f32 {
    match s.kind {
        WayKind::Road | WayKind::Path => (s.width / 3.0).clamp(0.0, 1.0) * 0.8,
        WayKind::Coastline => 0.60,
        WayKind::Water => 0.55,
        WayKind::Park => 0.45,
        WayKind::Building => 0.15,
        WayKind::Other => 0.20,
        // A named railway line (Västra stambanan, Malmbanan) is worth what a lake name is:
        // more than a residential street, less than the arterial it runs beside.
        WayKind::Rail => 0.55,
        // A tagless source carries no `name` to label with, by construction.
        WayKind::Overprint => 0.0,
        // A trail is not a named feature — it is a connection drawn between two that are.
        WayKind::Glow => 0.0,
    }
}

/// **A road number is not a road number** — the `(tier, rank)` a route `ref` earns.
///
/// Every `ref` used to be one thing: `Lod::Region`, rank `0.90`, above every street
/// name in the country. That is right for `E4` and wrong for `10061`. Sweden numbers
/// its *secondary county roads* with four- and five-digit refs, OSM carries them, and
/// the first calm z12 render of Stockholm came back labelled `35002 · 69013 · 10357 ·
/// 10060 · 10042 · 21001 · 10982 · 13506 · 40107` — the pane's whole label budget spent
/// on numbers no driver navigates by, because they outranked "Sveavägen" by
/// construction. Worse, `Lod::Region` meant they were eligible from **zoom 6**.
///
/// The ladder is the Swedish road hierarchy, read off the ref itself (there is no tag
/// that states it):
///
/// | ref            | example  | tier     | rank |
/// |----------------|----------|----------|------|
/// | European       | `E4`     | Region   | 0.95 |
/// | 1–2 digits     | `73`     | Region   | 0.88 |
/// | 3 digits       | `222`    | City     | 0.70 |
/// | 4+ digits      | `10061`  | City     | 0.22 |
///
/// `0.22` sits deliberately **below** a residential street (`0.27`): a five-digit
/// county road number carries less for a reader than the name of the street it is on.
fn ref_class(r: &str) -> (Lod, f32) {
    let digits = r.chars().filter(char::is_ascii_digit).count();
    if r.trim_start().starts_with(['E', 'e']) && digits > 0 {
        return (Lod::Region, 0.95);
    }
    match digits {
        0 => (Lod::City, 0.30),
        1..=2 => (Lod::Region, 0.88),
        3 => (Lod::City, 0.70),
        _ => (Lod::City, 0.22),
    }
}

/// Extract a [`LabelClass`] from an OSM way/node's `tags`, or `None` when the
/// feature carries no `name`/`ref` worth labelling. Priority + LOD tiering:
///
/// * a `place=` node → the settlement `name`, tiered by rank (country/state →
///   [`Lod::Country`], city/town → [`Lod::Region`], everything smaller →
///   [`Lod::City`]) — the country→region→city declutter backbone.
/// * a `highway` with a route `ref` (`E4`, `Rv40`) → the shield ref, tiered + ranked
///   by the ref's own class ([`ref_class`]: an `E`-road reads from a regional
///   overview, a five-digit county number does not); else its street `name` at
///   [`Lod::City`] (street names only once zoomed in).
/// * any other **named** feature (a lake, a park) → its `name` at [`Lod::City`].
///
/// Mirrors [`classify`]'s tag-priority discipline so the label pass and the fill
/// pass agree on what a feature *is* (REUSE: one tag vocabulary, no twinned rules).
pub fn classify_label<K: AsRef<str>, V: AsRef<str>>(tags: &[(K, V)]) -> Option<LabelClass> {
    let get = |k: &str| crate::tag(tags, k);
    let named = |lod: Lod, rank: f32| {
        get("name").filter(|s| !s.is_empty()).map(|n| LabelClass { text: n.to_string(), lod, rank })
    };

    // Settlement labels — the country→region→city declutter backbone.
    if let Some(place) = get("place") {
        let (lod, rank) = match place {
            "country" | "state" | "province" | "region" => (Lod::Country, 1.00),
            // ── A COUNTRY VIEW WITH NO NAMES IS NOT A MAP (`label-city-country-1`) ────
            //
            // `city` and `town` used to share `Lod::Region`, whose rung opens at camera
            // **6**. A whole-Sweden pane sits at about camera 5, so it opened NO rung but
            // Country — and the only Country-tier node in the entire cut is the single
            // `place=country` for Sverige. MEASURED on the live client 2026-08-31 at
            // camera 5.6: `labels` **15 797 fed / 0 drawn**. Not one name on the country
            // view.
            //
            // Splitting the pair is safe because the counts are not close. Named `place=*`
            // over all nineteen z6 tiles of the 2026-08-31 Sweden cut: `country` **1**,
            // `region` **5**, **`city` 6**, `municipality` 132, `town` 67. Six city names
            // on a pane showing a whole country is the minimum a map carries, not clutter
            // — and the areal budget (~65 names) is nowhere near reached by six.
            //
            // `town` stays at Region: 67 of them is right for a regional pane and wrong
            // for a continental one.
            "city" => (Lod::Country, 0.97),
            "town" => (Lod::Region, 0.95),
            // ── THE RUNG NOTHING STOOD ON (`label-district-1`) ───────────────────────
            //
            // `LOD_TIER_ZOOMS` is `[0, 6, 9, 12, 15]` — Country · Region · **District** ·
            // City · Detail. Every settlement class used to resolve to Country, Region or
            // City, so **District was occupied by no settlement at all** and the ladder
            // had a six-level dead band: a town appeared at camera 6 and then nothing new
            // was named until camera 12. That band, roughly 300 km down to 40 km of pane,
            // is the band a person navigates in.
            //
            // MEASURED on the live client 2026-08-31 (korp-ui b937078 against oden) at
            // camera 11.3: `labels` **5 319 fed / 1 drawn**, against a pane budget of
            // about 65 ([`LABEL_MIN_PX2_EACH`]). The placer was not overloaded, it was
            // starved — there was simply nothing in the viewport whose tier had opened.
            //
            // WHICH class belongs on the rung is a question about DENSITY, and it was
            // measured rather than chosen. Named `place=*` nodes over all nineteen z6
            // tiles of the 2026-08-31 Sweden cut:
            //
            // | class | named nodes |
            // |---|---:|
            // | `isolated_dwelling` | 25 221 |
            // | `hamlet` | 14 348 |
            // | `locality` | 3 687 |
            // | **`village`** | **996** |
            // | `municipality` | 132 |
            // | `town` | 67 |
            //
            // 996 villages over a whole country is regional density — a handful on a
            // 100 km pane, which the areal budget then ranks. 14 348 hamlets and 25 221
            // isolated dwellings are NOT, and they stay at City where they were.
            //
            // WHY NOT POPULATION, which would be the obvious refinement: because the data
            // does not carry it. Measured on the same tiles, `population` is present on
            // **97 % of towns** (which already draw at Region and gain nothing) and on
            // **27 % of villages, 2 % of hamlets and 0 % of isolated dwellings**. A tier
            // computed from a field that is absent for three quarters of the class it
            // decides is a tier decided by its own fallback.
            //
            // `municipality` rides with it: 132 nodes, and a kommun name is a regional
            // fact by definition. `suburb` deliberately does NOT — a suburb sits inside a
            // city whose name is already drawn two rungs earlier, so promoting it buys a
            // second name for one place.
            "village" => (Lod::District, 0.80),
            "municipality" | "borough" => (Lod::District, 0.85),
            // suburb / hamlet / neighbourhood / locality / isolated_dwelling / farm / islet …
            _ => (Lod::City, 0.65),
        };
        return named(lod, rank);
    }
    // Roads — a route ref is a shield (shows regionally); a street name is city-LOD.
    // The rank comes from `classify_with`'s own width table via `label_rank_of_style`
    // (ONE highway hierarchy, not a second `match hw` free to drift from it); the
    // palette only decides colours, which a rank does not read.
    if get("highway").is_some() {
        if let Some(r) = get("ref").filter(|s| !s.is_empty()) {
            let (lod, rank) = ref_class(r);
            return Some(LabelClass { text: r.to_string(), lod, rank });
        }
        return named(Lod::City, label_rank_of_style(&classify_with(tags, &MapPalette::cartographic(true))));
    }
    // Any other named feature (a water body, a park) → its name, zoomed-in only.
    named(Lod::City, label_rank_of_style(&classify_with(tags, &MapPalette::cartographic(true))))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn tags(kv: &[(&str, &str)]) -> Vec<(String, String)> {
        kv.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
    }

    /// **THE TWO ROSTERS MUST NAME THE SAME CLASSES AT THE SAME ZOOMS.**
    ///
    /// korp's `map_networks.rs::tiers` enumerates **22** `highway` classes and admits
    /// each from a floor LOD; this file enumerated **20**. The six korp names and facett
    /// did not all fell through to `_ => Lod::City`, and the fallback is not a tier
    /// policy — it is the absence of one:
    ///
    /// | class | korp admits from | facett drew from |
    /// |---|---|---|
    /// | `road`, `busway`, `bus_guideway` | **9** | 12 — three zooms LATE |
    /// | `raceway`, `rest_area`, `services` | **12** | 12 — right, by accident |
    ///
    /// The first three are the defect: korp cuts, stores, ships and decodes them from
    /// camera 9 and the painter withheld them until 12. The second three were already
    /// on the correct tier, but only because the fallback happened to point at it —
    /// correct by accident is not correct, and it is why they are pinned here too.
    ///
    /// **This is only expressible because of the z9 rung.** `Lod::District` did not
    /// exist before `98e4a316`; there was no tier at camera 9 for the first three to be
    /// aligned to, and the honest fix would have been unavailable.
    ///
    /// The oracle is korp's floor, not a restated tier name, so this fails if either
    /// side moves. What it CANNOT see is korp editing its own roster — that copy is
    /// guarded the same way `lod_ladder_vs_pyramid`'s pyramid copy is, by korp's own
    /// cross-seam test reading both originals.
    #[test]
    fn every_class_korp_sends_is_drawn_at_the_zoom_korp_sends_it() {
        let pal = MapPalette::cartographic(true);
        let draws_from =
            |hw: &str| crate::layer::LOD_TIER_ZOOMS[classify_with(&tags(&[("highway", hw)]), &pal).lod as usize];

        // (class, the floor korp admits it from — `map_networks.rs::tiers`, Network::Roads)
        let roster: [(&str, u8); 6] = [
            ("road", 9),
            ("busway", 9),
            ("bus_guideway", 9),
            ("raceway", 12),
            ("rest_area", 12),
            ("services", 12),
        ];
        let mut late = Vec::new();
        for (hw, korp_floor) in roster {
            let facett_from = draws_from(hw);
            if facett_from != korp_floor {
                late.push(format!(
                    "highway={hw}: korp sends from camera {korp_floor}, facett draws from \
                     {facett_from}"
                ));
            }
        }
        assert!(
            late.is_empty(),
            "{} of {} korp-enumerated classes are drawn at a camera other than the one \
             korp sends them from:\n  {}\n\
             Drawing LATER than the server sends wastes the bytes for those zooms; \
             drawing EARLIER claims a visibility that does not exist. Both are the \
             absence of a tier policy showing through `_`.",
            late.len(),
            roster.len(),
            late.join("\n  "),
        );

        // NON-VACUITY: the oracle must be able to distinguish tiers at all, or "they all
        // match" is a statement about one number.
        assert_ne!(draws_from("road"), draws_from("raceway"), "the two floors are different tiers");
        assert_eq!(draws_from("motorway"), 0, "…and the ladder still starts at 0");
    }

    /// **A NATIONAL RAIL NETWORK MUST BE ON SCREEN AT THE ZOOM A COUNTRY IS** (`railtier-1`).
    ///
    /// The assertion is the DRAWN predicate — `layer::lod_visible`, the exact `lod as u32
    /// <= view_lod_tier(zoom)` compare all three CPU layers and `cull.wgsl` draw through —
    /// and NOT the tier name and NOT a fed count. That distinction is the whole defect:
    /// on the owner's run at camera 9.33 the `rails` layer reported **7 217 fed** and drew
    /// **0**, so every count on the pane was healthy while the network was absent. A test
    /// asserting "the tier is District" would also pass on a build where `lod_visible`
    /// stopped being called at all.
    ///
    /// **THE RED IT WAS WRITTEN AGAINST.** Before the `railway` arm existed, `railway=rail`
    /// fell through to the generic arm's `Lod::City`, and this test failed on the first
    /// four cameras it checks — 9.0, 9.33, 10.0 and 11.0 — reporting `railway=rail is not
    /// drawn at camera 9 (tier City, the view is at tier 2)`. It passes after because the
    /// arm puts trunk rail on `Lod::Country`, and Country draws at every camera there is.
    #[test]
    fn a_national_railway_is_drawn_at_the_camera_a_country_is_looked_at_from() {
        let pal = MapPalette::cartographic(true);
        let sty = |kv: &[(&str, &str)]| classify_with(&tags(kv), &pal);
        let drawn = |s: &WayStyle, zoom: f32| {
            crate::layer::lod_visible(
                s.lod,
                &crate::layer::MapView::new(crate::layer::LatLon::new(59.33, 18.07), zoom, (900.0, 620.0)),
            )
        };

        let rail = sty(&[("railway", "rail")]);
        let tram = sty(&[("railway", "tram")]);
        let siding = sty(&[("railway", "platform")]);

        // ── THE CAMERA HE WAS ON. 9.33 is the reading off `$KORP_STATE`; the neighbours
        // are here so the pass is a RANGE and not one lucky rung.
        for z in [9.0_f32, 9.33, 10.0, 11.0, 6.0, 0.0] {
            assert!(
                drawn(&rail, z),
                "railway=rail is not drawn at camera {z} (tier {:?}, the view is at tier {}) \
                 — the server ships the whole national net from camera 0 \
                 (`map_networks::Network::tier_floors` gives Rails floor 0) and the painter \
                 is throwing it away",
                rail.lod,
                crate::layer::view_lod_tier(z),
            );
        }

        // ── THE TIERS ARE korp's OWN FLOORS, read through the ladder rather than named.
        // `[0, 9, 12]` is `tier_floors(Rails)`; if either side moves, this falls.
        let floor = |s: &WayStyle| crate::layer::LOD_TIER_ZOOMS[s.lod as usize];
        assert_eq!(floor(&rail), 0, "trunk rail rides korp's first Rails rung (floor 0)");
        assert_eq!(floor(&tram), 9, "urban rail rides korp's second Rails rung (floor 9)");
        assert_eq!(floor(&siding), 12, "yards and platforms ride korp's third rung (floor 12)");

        // ── NON-VACUITY, both directions. A ladder that draws everything at every zoom
        // would satisfy every assertion above, so the tiers must still DECLUTTER: a
        // platform must be absent at the camera the trunk line is present at.
        assert!(!drawn(&siding, 9.33), "a platform must NOT draw at camera 9.33");
        assert!(!drawn(&tram, 6.0), "a tram line must NOT draw at camera 6");
        assert!(drawn(&siding, 12.0), "…but the platform arrives at 12, or the tier is a wall");

        // ── AND IT IS NO LONGER THE GENERIC ARM. The tell that the fall-through is gone:
        // an untagged way and a railway used to be byte-identical styles.
        let generic = sty(&[("barrier", "fence")]);
        assert_ne!(rail.color, generic.color, "a rail must not paint in the untagged-way ink");
        assert_ne!(rail.lod, generic.lod, "…nor sit on the untagged-way tier");
        assert_eq!(generic.lod, Lod::City, "the generic arm itself has not moved");
    }

    /// **A RAILWAY IS A CASED TWO-TONE LINE IN ITS OWN KIND AND ITS OWN INK** — GG #28.
    ///
    /// # What "two-tone" can mean on this renderer, honestly
    ///
    /// The Garmin rail is a dark casing with light dashes. When this test was written the
    /// GPU line lane had NO dash machinery — `LineVertex` carried no arc length (it is
    /// exactly 48 B and that size is load-bearing), `line.wgsl` had no dash term, and the
    /// CPU painter stroked one `Shape::line` — so this test pinned the half both lanes
    /// could draw identically: the palette's casing under a core that is visibly a
    /// different tone from it. HO-T3 added the dashes on both lanes without growing the
    /// vertex (`gpu::line::pack_dash_half_px`, `tests/rail_dash_pixels.rs`); the tone
    /// contract here is unchanged and still what the gaps are cut out of.
    ///
    /// # THE RED
    ///
    /// * `railway=rail` classified `WayKind::Other` — there was no `Rail` (compile error).
    /// * On a LIGHT host theme through `from_theme`, the casing is `land` 55 % toward black
    ///   and the rail ink was `land` 62 % toward black: two greys 17 sRGB units apart, one
    ///   dark line, no second tone.
    #[test]
    fn a_railway_is_a_cased_two_tone_line_in_its_own_ink() {
        // A light "paper" host theme through `from_theme`, the branch where the core and the
        // casing were the same grey. Every theme in `Theme::ALL` is dark, so it is built here.
        let paper = Theme { bg: Color32::from_rgb(250, 250, 250), ..Theme::default() };
        let palettes = [
            ("from_theme(light)", MapPalette::from_theme(&paper)),
            ("from_theme(dark)", MapPalette::from_theme(&Theme::default())),
            ("cartographic(day)", MapPalette::cartographic(true)),
            ("cartographic(night)", MapPalette::cartographic(false)),
        ];
        let dist = |a: Color32, b: Color32| {
            (i32::from(a.r()) - i32::from(b.r()))
                .abs()
                .max((i32::from(a.g()) - i32::from(b.g())).abs())
                .max((i32::from(a.b()) - i32::from(b.b())).abs())
        };
        for (name, pal) in &palettes {
            let sty = |kv: &[(&str, &str)]| classify_with(&tags(kv), pal);
            let rail = sty(&[("railway", "rail")]);
            let tram = sty(&[("railway", "tram")]);
            let other = sty(&[("barrier", "fence")]);

            // ── ITS OWN KIND. The network proper, both rungs.
            for (v, s) in [("rail", &rail), ("narrow_gauge", &sty(&[("railway", "narrow_gauge")])), ("tram", &tram)] {
                assert_eq!(s.kind, WayKind::Rail, "{name}: railway={v} must be WayKind::Rail");
            }
            for v in ["light_rail", "subway", "funicular", "preserved", "miniature"] {
                assert_eq!(sty(&[("railway", v)]).kind, WayKind::Rail, "{name}: railway={v} is urban rail");
            }
            // …and the ground truth about the ground is NOT the network: a platform is not a
            // line you can ride, a disused bed carries no train, a construction site is a
            // promise. They keep the third rung and the catch-all kind they had.
            for v in ["abandoned", "disused", "razed", "construction", "platform", "station", "turntable"] {
                let s = sty(&[("railway", v)]);
                assert_ne!(s.kind, WayKind::Rail, "{name}: railway={v} must NOT become Rail");
                assert_eq!(s.lod, Lod::City, "{name}: railway={v} keeps the third rung");
            }

            // ── CASED. Both painters case every OPAQUE line (`paint_features` pass 1 and
            // the GPU casing pass) and skip a translucent one — so an opaque, non-area line
            // IS a cased line, and the rail must be that.
            assert_eq!(rail.color.a(), u8::MAX, "{name}: a translucent stroke gets no casing");
            assert!(!rail.is_area && rail.fill.is_none(), "{name}: a rail is a LINE, not a ring");

            // ── ITS OWN INK, from the ONE writer. Not `Other`'s, not a twin of `rail()`.
            assert_eq!(rail.color, pal.rail(), "{name}: the rail core is `MapPalette::rail()`");
            assert_eq!(tram.color, pal.rail(), "{name}: urban rail shares the rail ink");
            assert_ne!(rail.color, other.color, "{name}: a rail must not paint in Other's ink");
            assert_ne!(rail.color, pal.road_generic, "{name}: …nor in `road_generic`");

            // ── TWO TONES. The core must be told apart from the casing it sits in, and
            // from the paper it sits on — or it is one dark line and the casing is invisible.
            assert!(
                dist(rail.color, pal.casing) >= 40,
                "{name}: rail core {:?} and casing {:?} are one tone — no second tone to see",
                rail.color,
                pal.casing
            );
            assert!(
                dist(rail.color, pal.land) >= 20,
                "{name}: rail core {:?} vanishes into the paper {:?}",
                rail.color,
                pal.land
            );
            // Where the casing is dark, the core is the LIGHTER of the two: dark edges, light
            // body — the Garmin reading (the dashes are HO-T3's, cut out of this core).
            if relative_luminance(pal.casing) < 0.25 {
                assert!(
                    relative_luminance(rail.color) > relative_luminance(pal.casing),
                    "{name}: a dark casing wants a lighter core, got {:?} inside {:?}",
                    rail.color,
                    pal.casing
                );
            }
        }

        // ── AND A RESKIN KEEPS IT. `recolor_way` reskins BY KIND; an `Other` rail would come
        // back from the first `cartographic()` swap in `road_generic`.
        let night = MapPalette::cartographic(false);
        let day = MapPalette::cartographic(true);
        let mut w = crate::Way::from_lonlat_with(1, &[(18.0, 59.0), (18.01, 59.0)], &tags(&[("railway", "rail")]), &night)
            .expect("a 2-vertex line is a way");
        assert_eq!(w.stroke, night.rail());
        day.recolor_way(&mut w);
        assert_eq!(w.stroke, day.rail(), "a reskinned rail is still in the rail ink");
        assert_ne!(w.stroke, day.road_generic, "…and not in `Other`'s");
    }

    /// **A MARKED ROUTE IS NOT STREET FURNITURE** (`railtier-1`, the trail half).
    ///
    /// 19 664 of the 21 658 hiking rows in korp's Swedish cut carry `highway=path`, so the
    /// `highway` arm tiered them [`Lod::Detail`] — camera 15, the rung argued for footpaths
    /// in a city. MEASURED on the live client at camera 9.33: `trails` fed **6 719**, drew
    /// **32**.
    ///
    /// **THE RED.** Before the route arm existed, the four `drawn_at_his_camera` assertions
    /// failed, reporting e.g. `a hiking path (sac_scale) is not drawn at camera 9.33 — tier
    /// Detail, view tier 2`. Every one of them is asserted through `layer::lod_visible`, the
    /// same DRAWN predicate the painter uses, so a build that stopped calling the ladder
    /// cannot pass this by having the right tier name.
    ///
    /// The rule ORDER is the other half of the test, and it is not cosmetic: `mtb:scale`
    /// rides 100 % of the `mtb` class and 25 % of `hiking`, so asking it before the hiking
    /// tags takes a quarter of the hiking network down to camera 12. The two
    /// `hiking_beats_mtb_scale` cases pin that ordering.
    #[test]
    fn a_marked_route_is_drawn_before_the_footpath_rung() {
        let pal = MapPalette::cartographic(true);
        let sty = |kv: &[(&str, &str)]| classify_with(&tags(kv), &pal);
        let drawn = |s: &WayStyle, zoom: f32| {
            crate::layer::lod_visible(
                s.lod,
                &crate::layer::MapView::new(crate::layer::LatLon::new(59.33, 18.07), zoom, (900.0, 620.0)),
            )
        };
        let floor = |s: &WayStyle| crate::layer::LOD_TIER_ZOOMS[s.lod as usize];

        // The four rows a marked route really arrives as — tags off the real cut, never
        // invented: a hiking path scored on the SAC scale, one with only its visibility
        // tagged, a piste, and a cycling route carrying the relation's own `route`.
        let sac = sty(&[("highway", "path"), ("sac_scale", "mountain_hiking")]);
        let vis = sty(&[("highway", "path"), ("trail_visibility", "excellent")]);
        let piste = sty(&[("piste:type", "nordic"), ("name", "Vasaloppsspåret")]);
        let cycle = sty(&[("highway", "cycleway"), ("route", "bicycle")]);
        for (what, s) in [("sac_scale", &sac), ("trail_visibility", &vis), ("piste:type", &piste), ("route", &cycle)] {
            assert!(
                drawn(s, 9.33),
                "a trail row identified by {what} is not drawn at camera 9.33 — tier {:?}, \
                 view tier {}. korp serves 41.8 % of the trail file at this rung; the \
                 painter was drawing 0.5 % of what it was handed.",
                s.lod,
                crate::layer::view_lod_tier(9.33),
            );
            assert_eq!(floor(s), 9, "…and it rides korp's own second Trails rung (floor 9)");
        }

        // ── LOCAL DETAIL STAYS LOCAL, which is korp's third rung and the non-vacuity.
        // 31 340 snowmobile ways and 12 724 mtb ways are the biggest classes in the file
        // and korp itself calls them local detail; a rule that lifted everything would
        // satisfy every assertion above.
        let sled = sty(&[("snowmobile", "designated"), ("ice_road", "yes")]);
        let mtb = sty(&[("highway", "path"), ("mtb:scale", "2")]);
        assert!(!drawn(&sled, 9.33), "a snowmobile track must NOT draw at camera 9.33");
        assert!(!drawn(&mtb, 9.33), "an mtb trail must NOT draw at camera 9.33");
        assert_eq!(floor(&sled), 12, "snowmobile rides korp's third Trails rung (floor 12)");
        assert_eq!(floor(&mtb), 12, "…as does mtb");
        assert!(drawn(&sled, 12.0), "…and both arrive at 12, or the tier is a wall");

        // ── ORDER: the hiking tags are asked BEFORE `mtb:scale`, because `mtb:scale` is on
        // a quarter of the hiking rows (5 374 of 21 658). Reverse the two `if`s and these
        // two fall.
        let hiking_also_biked = sty(&[("highway", "path"), ("sac_scale", "hiking"), ("mtb:scale", "1")]);
        assert_eq!(hiking_also_biked.lod, Lod::District, "sac_scale must win over mtb:scale");
        let vis_also_biked = sty(&[("highway", "path"), ("trail_visibility", "good"), ("mtb:scale", "3")]);
        assert_eq!(vis_also_biked.lod, Lod::District, "trail_visibility must win over mtb:scale");

        // ── A ROUTE ALONG AN ARTERIAL IS STILL AN ARTERIAL. 343 rows in the trails cut
        // are major roads carrying a route relation's tags; demoting them would move a
        // primary road two tiers down and take it off the country view.
        let primary_route = sty(&[("highway", "primary"), ("route", "bicycle"), ("ref", "E4")]);
        let primary = sty(&[("highway", "primary")]);
        assert_eq!(primary_route.lod, primary.lod, "a route along a primary must stay a primary");
        assert_eq!(primary_route.color, primary.color, "…in road ink, not trail ink");
        assert_eq!(floor(&primary), 0, "…and the primary tier itself has not moved");

        // ── AND A PLAIN FOOTPATH IS UNTOUCHED. The arm must not have swallowed the rung
        // it sits above: 721 of 75 694 trail rows match nothing and fall through here,
        // and so does every footpath in a city.
        let footway = sty(&[("highway", "footway")]);
        assert_eq!(footway.lod, Lod::Detail, "a plain footway keeps the Detail rung");
        assert!(!drawn(&footway, 9.33), "…and is still absent at camera 9.33");
    }

    /// **A FOREST ROAD YOU CAN DRIVE DOWN IS NOT A FOOTPATH.**
    ///
    /// `highway=track` sat in the `footway | cycleway | path | pedestrian | steps` arm,
    /// so it was painted in `pal.path` at the footpath's 0.7 width and classified
    /// `WayKind::Path`. The arm's own comment justifies itself with `kalm-1` — *"the
    /// entire footpath network of a city drew at zoom 12, where it is sub-pixel"* — which
    /// is a RENDERING-DENSITY argument, and a good one, for footpaths. `track` was swept
    /// in with them and never argued for on its own.
    ///
    /// It is a vehicular road by OSM definition: agricultural and forestry roads you can
    /// drive. korp's own histogram of the Sweden extract names it exactly that —
    /// *"`track` (316 960 — forest and field roads)"* — and files it with `service` in
    /// the minor VEHICULAR tail, in a tier whose comment says *"and nothing pedestrian"*.
    ///
    /// So: road colour, road width, `WayKind::Road`, matching `service` because korp
    /// treats them as the same class of thing and any difference here would be the same
    /// unargued taste call that created the defect.
    ///
    /// **THE TIER DOES NOT MOVE, and this is the half a future reader will get wrong.**
    /// `Lod::Detail` stays. facett draws `track` from camera 15; korp sends it from 14.
    /// The client drawing LATER than the server sends is the benign direction — one zoom
    /// of bytes that arrive before anything paints them. The dangerous direction is the
    /// opposite, a client wanting to draw what the server withholds, and that is not what
    /// this is. The two ladders have different granularity BY DESIGN (five cartographic
    /// tiers against korp's six wire floors), so exact alignment is neither possible nor
    /// desirable; "helpfully" moving this tier to 14 to match korp would introduce a
    /// mismatch across two ladders that are meant to be independent.
    /// **A FERRY IS NOT A FOOTPATH.** `route=ferry` used to fall into the marked-route
    /// arm below `has("route")` and be stroked in `pal.path` — the brown track ink —
    /// from `Lod::District` (camera 9) upward, so an international ferry line was
    /// painted across open sea as a long straight brown chord. The owner photographed it
    /// over Skåne.
    ///
    /// MEASURED on the served Swedish cut (`~/.korp/work/sweden/ways.parquet`,
    /// 2026-09-02, 9 325 142 rows): **876** ways carry `route=ferry` or `ferry=*`, and
    /// **772 of them sit at `min_zoom` 9**, so the server hands them over at exactly the
    /// rung the trail arm drew them at. The longest is way **299847757**, *"København
    /// (DK) – Oslo (N)"*, whose bbox spans **2.656° of latitude** (55.929 → 58.585) and
    /// **2.037° of longitude** (10.656 → 12.693) — one straight line right across the
    /// Kattegat, four hundred kilometres of brown track ink over water.
    ///
    /// The tags below are that way's own, copied from the file.
    #[test]
    fn a_ferry_route_is_a_sea_route_not_a_footpath() {
        let pal = MapPalette::cartographic(true);
        let sty = |kv: &[(&str, &str)]| classify_with(&tags(kv), &pal);

        // Way 299847757 exactly as the cut carries it.
        let cruise = sty(&[
            ("duration", "17:30"),
            ("ferry", "cruiseferry"),
            ("name", "København (DK) – Oslo (N)"),
            ("network", "international"),
            ("route", "ferry"),
        ]);
        // The other shapes the file really uses: `ferry` carrying a HIGHWAY CLASS as its
        // value (this is why it reads as a road — `ferry=trunk` on 707334351, Travemünde
        // – Helsinki), and a bare `route=ferry` with no `ferry` key (1139737055,
        // Trelleborg – Karlshamn).
        let trunk_ferry = sty(&[("ferry", "trunk"), ("route", "ferry"), ("name", "Travemünde (D) – Helsinki (FIN)")]);
        let bare = sty(&[("route", "ferry"), ("name", "Trelleborg - Karlshamn")]);
        // …and a ferry tagged only on the `ferry` key, no `route` at all.
        let ferry_only = sty(&[("ferry", "yes")]);

        for (what, s) in [
            ("route=ferry + ferry=cruiseferry", &cruise),
            ("ferry=trunk", &trunk_ferry),
            ("bare route=ferry", &bare),
            ("ferry=yes", &ferry_only),
        ] {
            assert_ne!(
                s.color, pal.path,
                "{what} is painted in the FOOTPATH ink (#{:02X}{:02X}{:02X}) — that is the \
                 brown chord across the Kattegat",
                pal.path.r(), pal.path.g(), pal.path.b(),
            );
            assert_eq!(s.color, pal.coast, "{what} is a sea route and takes the marine ink");
            assert_eq!(s.kind, WayKind::Water, "{what} is water, not a path you can walk");
            assert!(!s.is_area, "{what} is a LINE — a ferry route never closes into a filled ring");
            assert_eq!(
                s.lod,
                Lod::City,
                "{what} draws from the city rung. A ferry link is information when the \
                 operator is looking at a harbour; a 2.66°-long chord over a regional view \
                 is the clutter that was reported.",
            );
        }

        // ── THE NON-VACUITY. A marked LAND route is untouched: it still takes the trail
        // arm, the path ink and the District rung. A rule that demoted every `route` to
        // water would satisfy every assertion above.
        let hike = sty(&[("highway", "path"), ("route", "hiking"), ("sac_scale", "hiking")]);
        assert_eq!(hike.color, pal.path, "a hiking route is still a path");
        assert_eq!(hike.kind, WayKind::Path, "…and still a Path");
        assert_eq!(hike.lod, Lod::District, "…on the trail rung it has always had");
    }

    /// **The painted-key roster is exactly the keys this crate reads** — the guard that
    /// makes [`PAINTED_KEYS`] safe for a server to filter a wire against (korp lane
    /// GG #19 row 2).
    ///
    /// A stale roster does not fail loudly on a server: it silently stops sending a key,
    /// and a building loses its height or a motorway its shield, on some cameras, for
    /// some features. So the roster is scanned out of this crate's own source rather
    /// than trusted, in BOTH directions.
    ///
    /// **What counts as a tag read**, and why the rule is written this way:
    ///
    /// * `tag(tags, "…")` / `has_tag(tags, "…")` — the two door functions, unambiguous;
    /// * `get("…")` / `has("…")` — the closures every painter opens with
    ///   (`let get = |k: &str| crate::tag(tags, k);`). A leading `.` is REQUIRED to be
    ///   absent, because `obj.get("pan")` on a `serde_json::Value` is not a tag read and
    ///   this file is full of them.
    ///
    /// Test code is cut at the first `#[cfg(test)]` and comment lines are dropped — a
    /// key named in prose is not a key the crate reads, and the arms here quote plenty.
    #[test]
    fn the_painted_key_roster_is_every_key_this_crate_reads() {
        // Every module of this crate that could read a tag. Named explicitly: a glob
        // would silently stop covering a module added later, which is the same failure
        // this test exists to prevent.
        const SOURCES: [(&str, &str); 10] = [
            ("lib.rs", include_str!("lib.rs")),
            ("style.rs", include_str!("style.rs")),
            ("height.rs", include_str!("height.rs")),
            ("layerset.rs", include_str!("layerset.rs")),
            ("paint_order.rs", include_str!("paint_order.rs")),
            ("layer.rs", include_str!("layer.rs")),
            ("index.rs", include_str!("index.rs")),
            ("project.rs", include_str!("project.rs")),
            ("tessellate.rs", include_str!("tessellate.rs")),
            ("l0_points.rs", include_str!("l0_points.rs")),
        ];

        /// Collect the tag-key literals of one source file.
        fn keys_of(src: &str) -> Vec<String> {
            // Cut the tests off: a fixture's `("highway", "motorway")` is not a read,
            // and neither is a roster assertion.
            let code = src.split("#[cfg(test)]").next().unwrap_or(src);
            let code: String = code
                .lines()
                .filter(|l| !l.trim_start().starts_with("//"))
                .collect::<Vec<_>>()
                .join("\n");
            let b = code.as_bytes();
            let mut out = Vec::new();
            // `opener` is the text before the quoted key; `needs_free_start` marks the
            // forms where a preceding `.` would mean something else entirely.
            for (opener, needs_free_start) in [
                ("tag(tags, \"", false),
                ("has_tag(tags, \"", false),
                ("get(\"", true),
                ("has(\"", true),
            ] {
                let mut at = 0usize;
                while let Some(i) = code[at..].find(opener) {
                    let start = at + i;
                    let from = start + opener.len();
                    let Some(len) = code[from..].find('"') else { break };
                    let key = &code[from..from + len];
                    // `.get("pan")` on a serde_json Value is not a tag read.
                    let free = !needs_free_start
                        || start == 0
                        || !matches!(b[start - 1], b'.' | b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9');
                    // A key is lowercase ASCII with `:` and `_`; anything else is some
                    // other `get`.
                    let plausible = !key.is_empty()
                        && key.bytes().all(|c| c.is_ascii_lowercase() || matches!(c, b':' | b'_'));
                    if free && plausible {
                        out.push(key.to_string());
                    }
                    at = from + len;
                }
            }
            out
        }

        let mut read: Vec<String> = SOURCES.iter().flat_map(|(_, s)| keys_of(s)).collect();
        read.sort();
        read.dedup();

        // NON-VACUITY: the scan must have reached real bodies and found the keys nobody
        // would ever delete. Without this the test passes on an empty scan.
        assert!(
            read.len() >= 30,
            "the scan found only {} tag keys across the crate: {read:?} — it is not \
             reading the painters",
            read.len(),
        );
        for must in ["highway", "name", "place", "layer", "height", "amenity", "ref"] {
            assert!(
                read.iter().any(|k| k == must),
                "the scan missed `{must}`, which is read in plain sight — the scan rule \
                 is broken, and every conclusion below it is worthless",
            );
        }
        // Every CLASSIFIED key must be a PAINTED key: the classifier is one of the
        // painters, so a key that decides a class the wider roster does not name would
        // let a filter delete the class itself.
        for k in CLASSIFIED_KEYS {
            assert!(PAINTED_KEYS.contains(&k), "`{k}` classifies but is not in PAINTED_KEYS");
        }
        for (k, _) in CLASSIFIED_PAIRS {
            assert!(PAINTED_KEYS.contains(&k), "`{k}` classifies by value but is not in PAINTED_KEYS");
        }

        let missing: Vec<&String> = read.iter().filter(|k| !PAINTED_KEYS.contains(&k.as_str())).collect();
        assert!(
            missing.is_empty(),
            "these keys are READ by a painter and are NOT in PAINTED_KEYS: {missing:?} — a \
             server filtering its wire against this roster would silently stop sending \
             them, and the feature would lose that ink with nothing going red",
        );
        let spare: Vec<&&str> = PAINTED_KEYS.iter().filter(|k| !read.iter().any(|r| r == *k)).collect();
        assert!(
            spare.is_empty(),
            "PAINTED_KEYS names keys no painter reads: {spare:?} — a roster with dead \
             entries costs wire bytes for nothing and hides which reads are real",
        );
        // The roster is searched by `contains` on every tag of every way of every reply,
        // so keep it sorted: it is the property that makes a future binary search or a
        // human diff trustworthy, and a duplicate would be invisible otherwise.
        let mut sorted = PAINTED_KEYS.to_vec();
        sorted.sort_unstable();
        sorted.dedup();
        assert_eq!(sorted.as_slice(), &PAINTED_KEYS[..], "PAINTED_KEYS must be sorted and unique");
    }

    /// ★ **RED-when-broken: the [`CLASSIFIED_KEYS`] roster IS the keys
    /// [`classify_with`] reads.**
    ///
    /// A hand-written allowlist beside a 450-line match is a second reader waiting to
    /// drift, and the drift is silent in the dangerous direction: a new arm that reads a
    /// key missing from the roster makes [`has_nothing_to_draw`] refuse a way the
    /// classifier would have painted. So the roster is checked against the FUNCTION TEXT,
    /// not against a copy of itself (the identity-value trap LAW 2 names).
    ///
    /// **RED ARM** (proved before this was trusted): drop `"waterway"` from
    /// `CLASSIFIED_KEYS` and this fails with
    /// `classify_with reads keys the roster does not name: ["waterway"]`. Put it back:
    /// green. Adding a key the function never reads fails the other assertion.
    #[test]
    fn the_classified_key_roster_is_the_keys_classify_with_reads() {
        let src = include_str!("style.rs");
        let body_start = src.find("pub fn classify_with<K: AsRef<str>, V: AsRef<str>>").expect(
            "classify_with must be findable in this file's own text — if it was renamed, \
             this guard is scanning nothing and its silence proves nothing",
        );
        // The function ends where the generic fallback's closing brace does; the roster
        // const is declared immediately after it, so stop there.
        let body_end = body_start
            + src[body_start..].find("pub const CLASSIFIED_KEYS").expect(
                "CLASSIFIED_KEYS must follow classify_with in this file",
            );
        // CODE ONLY — the arms carry long prose that quotes tag keys, and a key named in
        // a comment is not a key the function reads.
        let body: String = src[body_start..body_end]
            .lines()
            .filter(|l| !l.trim_start().starts_with("//"))
            .collect::<Vec<_>>()
            .join("\n");
        let body = body.as_str();

        // Every `get("…")` / `has("…")` literal in the body, in source order.
        let mut read: Vec<String> = Vec::new();
        for opener in ["get(\"", "has(\""] {
            let mut at = 0usize;
            while let Some(i) = body[at..].find(opener) {
                let from = at + i + opener.len();
                let Some(len) = body[from..].find('"') else { break };
                read.push(body[from..from + len].to_string());
                at = from + len;
            }
        }
        read.sort();
        read.dedup();

        // NON-VACUITY, three ways: the scan reached a real body, it found a plausible
        // number of keys, and it found the ones nobody would ever delete.
        assert!(
            body.len() > 3_000,
            "the scan captured only {} bytes of classify_with — it is not looking at the \
             function",
            body.len(),
        );
        assert!(read.len() >= 12, "found only {} tag keys in classify_with: {read:?}", read.len());
        for must in ["highway", "natural", "building", "railway"] {
            assert!(read.iter().any(|k| k == must), "the scan missed `{must}` — it is not reading the arms");
        }

        // A key is admitted EITHER wholesale (`CLASSIFIED_KEYS`) or value by value
        // (`CLASSIFIED_PAIRS`) — never both, or the pair roster would be decoration
        // while the key roster silently let every value through (lane TL).
        let pair_keys: Vec<&str> = {
            let mut v: Vec<&str> = CLASSIFIED_PAIRS.iter().map(|(k, _)| *k).collect();
            v.sort();
            v.dedup();
            v
        };
        for k in &pair_keys {
            assert!(
                !CLASSIFIED_KEYS.contains(k),
                "`{k}` is in BOTH rosters — the key roster admits every value under it, so \
                 the pair roster is doing nothing and `{k}=<anything>` reaches the generic \
                 hairline",
            );
        }

        let missing: Vec<&String> = read
            .iter()
            .filter(|k| !CLASSIFIED_KEYS.contains(&k.as_str()) && !pair_keys.contains(&k.as_str()))
            .collect();
        assert!(
            missing.is_empty(),
            "classify_with reads keys neither roster names: {missing:?} — \
             `has_nothing_to_draw` would refuse a way the classifier can paint",
        );
        let spare: Vec<&&str> = CLASSIFIED_KEYS.iter().filter(|k| !read.iter().any(|r| r == *k)).collect();
        assert!(spare.is_empty(), "the roster names keys classify_with never reads: {spare:?}");
        let spare_pairs: Vec<&&str> = pair_keys.iter().filter(|k| !read.iter().any(|r| r == **k)).collect();
        assert!(spare_pairs.is_empty(), "CLASSIFIED_PAIRS names keys classify_with never reads: {spare_pairs:?}");
        // Every pair's VALUE must appear as a literal in the arm that reads its key, or
        // the pair roster admits a way the classifier then drops to the generic hairline
        // — the exact defect the pair roster exists to prevent.
        for (k, v) in CLASSIFIED_PAIRS {
            assert!(
                body.contains(&format!("Some(\"{v}\")")),
                "`{k}={v}` is admitted by CLASSIFIED_PAIRS but `{v}` is not matched anywhere in \
                 classify_with — it would fall to the generic `road_generic` hairline",
            );
        }
    }

    /// ★ **RED-when-broken: a way whose tags say NOTHING is not a road.**
    ///
    /// The E sea-ink defect, from the cut that produced it. Each `tags` block below is
    /// copied from a way in `~/.korp/work/sweden/ways.parquet` whose DRAWN LINE crosses
    /// the open water of the z14 Stockholm frame (`sweep_oracles.py` oracle E,
    /// `RED(4129)` on the 2026-09-03 live-cut sweep), stroked `road_generic` #DED8CB.
    ///
    /// **RED ARM**: revert either `has_nothing_to_draw` or the four constructor gates and
    /// the `is_none()` assertions fail — every one of these becomes a hairline road again.
    /// The second half is the non-vacuity: a way that carries a key the classifier reads
    /// still builds, so a gate that refused everything would not pass.
    #[test]
    fn a_way_with_nothing_to_draw_is_not_a_road() {
        let pal = MapPalette::cartographic(true);
        let line = [(18.05, 59.32), (18.07, 59.33)];

        // The real sea-crossers, by way id.
        let refused: [(&str, &[(&str, &str)]); 5] = [
            ("382768363 — a boundary relation's untagged member way", &[]),
            ("382768356 — kommungräns in Riddarfjärden", &[("source", "historical"), ("source_ref", "ekonomiska kartan")]),
            ("382769100 — the maritime boundary segment", &[("maritime", "yes")]),
            ("1207835868 — a submarine pipeline", &[("seamark:type", "pipeline_submarine")]),
            ("512933635 — a submarine power cable", &[("seamark:cable_submarine:category", "power"), ("seamark:type", "cable_submarine")]),
        ];
        for (what, kv) in refused {
            assert!(
                has_nothing_to_draw(&tags(kv)),
                "{what}: these tags name nothing classify_with reads",
            );
            assert!(
                crate::Way::from_lonlat_with(1, &line, &tags(kv), &pal).is_none(),
                "{what} still built a Way — it will be stroked #{:02X}{:02X}{:02X} across \
                 the water",
                pal.road_generic.r(), pal.road_generic.g(), pal.road_generic.b(),
            );
            assert!(
                crate::Way::from_lonlat_owned_with(1, line.to_vec(), &tags(kv), &pal).is_err(),
                "{what} still built a Way through the MOVING door",
            );
        }

        // ── NON-VACUITY. Everything the map is actually made of still builds, including
        // the classes that reach the generic arm through a key that IS read
        // (`railway=*` has no WayKind of its own and korp re-inks it from the network
        // column — refusing it here would delete the rail network).
        let kept: [(&str, &[(&str, &str)]); 6] = [
            ("a street", &[("highway", "residential")]),
            ("a rail way", &[("railway", "rail")]),
            ("a building", &[("building", "yes")]),
            ("a lake", &[("natural", "water")]),
            ("a coastline", &[("natural", "coastline")]),
            ("a ferry route", &[("route", "ferry")]),
        ];
        for (what, kv) in kept {
            assert!(!has_nothing_to_draw(&tags(kv)), "{what} has something to draw");
            assert!(
                crate::Way::from_lonlat_with(1, &line, &tags(kv), &pal).is_some(),
                "{what} must still build — the gate is about tags that say nothing, not \
                 about tags the arms do not MATCH",
            );
        }
    }

    /// ★ **RED-when-broken: the three classes lane SI named and did not decide.**
    ///
    /// SI's report: `man_made` / `place` / `boundary` "now draw nothing… each deserves
    /// its own arm and ink; inventing one silently would be a taste call". This is that
    /// call, made with frames (korp `tests/map_render_truth.rs`,
    /// `the_three_undecided_classes_get_their_verdicts_in_pixels`), and this guard holds
    /// it:
    ///
    /// | class | verdict | why |
    /// |---|---|---|
    /// | `boundary=*` | **correctly nothing** | it IS the defect — the kommungräns through Riddarfjärden. And facett already draws administrative units, from `facett-geomap`'s `sweden_admin`; a second boundary painter in the way path would be a twin (LAW 5) fed by member geometry the relation path already owns. |
    /// | `place=*` | **correctly nothing** | `place` on a way is a LABEL anchor, not a stroke. `place=islet` has no outline anyone wants; it has a name. The label/POI path (`LayerCategory::from_osm_tags`, `facett_osm::read_pois`) is where a place belongs and it already reads the key. |
    /// | `man_made=pier` | **its own ink** | ground you stand on. See the arm in [`classify_with`] and [`CLASSIFIED_PAIRS`]. |
    ///
    /// And the split INSIDE `man_made` is the whole reason it needed a pair roster:
    /// `pier` draws, `pipeline` does not, and two of lane SI's five measured sea-crossing
    /// chords were `seamark`/pipeline geometry.
    ///
    /// **RED ARM** (all seen 2026-09-03): give `man_made` a key-level admission (move it
    /// into [`CLASSIFIED_KEYS`]) and the `pipeline` assertion fails — it becomes a
    /// `road_generic` hairline again. Delete the `man_made` arm from [`classify_with`]
    /// and the pier assertions fail. Add `boundary` or `place` to either roster and the
    /// first two rows fail.
    #[test]
    fn the_three_undecided_classes_are_decided() {
        let pal = MapPalette::cartographic(true);
        let line = [(18.05, 59.32), (18.07, 59.33)];

        // ── boundary + place: correctly NOTHING. ─────────────────────────────────
        let nothing: [(&str, &[(&str, &str)]); 4] = [
            ("boundary=administrative (the kommungräns SI measured)", &[("boundary", "administrative")]),
            ("boundary=maritime", &[("boundary", "maritime")]),
            ("place=islet", &[("place", "islet")]),
            ("place=locality", &[("place", "locality")]),
        ];
        for (what, kv) in nothing {
            assert!(has_nothing_to_draw(&tags(kv)), "{what} draws nothing in the WAY path");
            assert!(
                crate::Way::from_lonlat_with(1, &line, &tags(kv), &pal).is_none(),
                "{what} still built a Way — it would stroke #{:02X}{:02X}{:02X} across the water",
                pal.road_generic.r(), pal.road_generic.g(), pal.road_generic.b(),
            );
        }

        // ── man_made: the values that are GROUND draw, in the paper ink. ──────────
        for v in ["pier", "breakwater", "groyne", "embankment", "causeway"] {
            let kv = [("man_made", v)];
            assert!(!has_nothing_to_draw(&tags(&kv)), "man_made={v} is ground and draws");
            let st = classify_with(&tags(&kv), &pal);
            assert_eq!(
                st.color, pal.land,
                "man_made={v} is land pushed into water and is inked as the paper, so it \
                 reads as a finger off the quay and vanishes over land — got \
                 #{:02X}{:02X}{:02X}",
                st.color.r(), st.color.g(), st.color.b(),
            );
            assert_eq!(st.lod, Lod::City, "…at the rung a metres-wide structure is seen from");
            assert_eq!(st.kind, WayKind::Path, "…and not a Road, or korp's network re-ink would claim it");
            assert!(crate::Way::from_lonlat_with(1, &line, &tags(&kv), &pal).is_some(), "man_made={v} builds");
        }

        // ── man_made: the values that are NOT on the surface still draw NOTHING. ──
        // The other half of the sea chords. THIS is why the admission had to be
        // value-level: a key-level one puts every one of these back on the water.
        for v in ["pipeline", "cable", "storage_tank", "silo", "surveillance"] {
            let kv = [("man_made", v)];
            assert!(
                has_nothing_to_draw(&tags(&kv)),
                "man_made={v} is not ground — a submarine pipeline was one of the five \
                 chords lane SI measured across Riddarfjärden, and it must stay refused",
            );
            assert!(crate::Way::from_lonlat_with(1, &line, &tags(&kv), &pal).is_none(), "man_made={v} is refused");
        }
    }

    /// ★ **RED-when-broken: the overprint ink is legible on the ground and is not
    /// mistakable for any basemap ink.**
    ///
    /// [`untagged_source_style`] exists because a tagless SOURCE must draw and must not
    /// draw as `road_generic` — the ink lane SI took off the water. Two DIFFERENT
    /// questions, and the first version of this guard used one metric for both and was
    /// wrong for it:
    ///
    /// * **legible ON the ground** (paper, water, green) is a LUMINANCE question, and
    ///   WCAG [`contrast_ratio`] is the tool. Held `>= 3.0`. The number to keep in view:
    ///   `road_generic` against day paper is **1.24** — the ink lane SI removed was
    ///   never visible on land at all; it only ever showed up because it was being drawn
    ///   across blue.
    /// * **tellable apart from another INK** is a HUE question, and contrast ratio
    ///   cannot answer it: the magenta `#B5197A` and the motorway orange `#E9A23B` sit
    ///   at `2.86:1` because they have nearly the same luminance, while being about as
    ///   confusable as two colours get, i.e. not at all. So per-channel distance, held
    ///   `>= 120` of a possible 765. MEASURED: the nearest day ink is `path` `#B99873`
    ///   at **138**, the nearest night ink is `road_secondary` `#8A9CB2` at **200**.
    ///
    /// **RED ARM** (both seen 2026-09-03): set `OVERPRINT_DAY` to `road_generic`'s
    /// `#DED8CB` and the distance row fails at `0`; set it to the building fill
    /// `#E2DBCF` and the paper row fails at `1.13:1`.
    #[test]
    fn the_overprint_ink_is_nobody_elses_ink() {
        /// Per-channel distance, `0..=765`. A luminance ratio cannot see hue, and
        /// "which lines came out of my file?" is a hue question.
        fn channel_distance(a: Color32, b: Color32) -> i32 {
            let d = |x: u8, y: u8| (i32::from(x) - i32::from(y)).abs();
            d(a.r(), b.r()) + d(a.g(), b.g()) + d(a.b(), b.b())
        }

        for day in [true, false] {
            let pal = MapPalette::cartographic(day);
            let st = untagged_source_style(&pal);
            assert_eq!(st.color, pal.overlay, "the overlay style paints the overlay ink");
            assert_eq!(st.lod, Lod::Region, "a national overlay is looked at from a national camera");
            assert!(st.width > 1.0, "an operator-loaded layer is a layer, not a hairline: {}", st.width);
            assert!(!st.is_area && st.fill.is_none(), "an overprint is strokes; a fill would hide the base sheet");

            // LEGIBILITY — against the three GROUNDS an overlay is drawn on top of.
            for (what, c) in [("the paper", pal.land), ("water", pal.water), ("green", pal.green)] {
                let r = contrast_ratio(pal.overlay, c);
                assert!(
                    r >= 3.0,
                    "day={day}: the overprint #{:02X}{:02X}{:02X} is only {r:.2}:1 against \
                     {what} #{:02X}{:02X}{:02X} — road_generic against day paper is 1.24:1 \
                     and that ink was invisible, which is half of how this defect hid",
                    pal.overlay.r(), pal.overlay.g(), pal.overlay.b(), c.r(), c.g(), c.b(),
                );
            }

            // DISTINGUISHABILITY — against EVERY other ink in the palette, named, so a
            // future ink added near the overprint fails here rather than on a screen.
            let inks: [(&str, Color32); 12] = [
                ("land", pal.land),
                ("water", pal.water),
                ("green", pal.green),
                ("building", pal.building),
                ("coast", pal.coast),
                ("road_motorway", pal.road_motorway),
                ("road_primary", pal.road_primary),
                ("road_secondary", pal.road_secondary),
                ("road_tertiary", pal.road_tertiary),
                ("road_residential", pal.road_residential),
                ("path", pal.path),
                ("road_generic (the ink lane SI removed)", pal.road_generic),
            ];
            for (what, c) in inks {
                let d = channel_distance(pal.overlay, c);
                assert!(
                    d >= 120,
                    "day={day}: the overprint #{:02X}{:02X}{:02X} is only {d} of 765 from \
                     {what} #{:02X}{:02X}{:02X} — an overlay you cannot tell from the \
                     basemap answers no question",
                    pal.overlay.r(), pal.overlay.g(), pal.overlay.b(), c.r(), c.g(), c.b(),
                );
            }
        }

        // ── THE RESKIN. A palette swap must not repaint the operator's overlay.
        //
        // FOUND BY `facett-demo`'s deck guard, 2026-09-03: the overprint style was
        // `WayKind::Other` at first, `recolor_way` maps `Other` to `road_generic`, and
        // `OsmView::cartographic(true)` calls `recolor` on the way in — so a correctly
        // read overlay was repainted into the exact ink lane SI took off the water,
        // before anything drew. `WayKind::Overprint` is what makes the ink survive.
        let ring = [(9.50, 47.10), (9.52, 47.10), (9.52, 47.12)];
        let day = MapPalette::cartographic(true);
        let night = MapPalette::cartographic(false);
        let mut w = crate::Way::from_lonlat_owned_styled(1, ring.to_vec(), crate::NO_TAGS, untagged_source_style(&day))
            .expect("the non-classifying door builds it");
        assert_eq!(w.stroke, day.overlay);
        night.recolor_way(&mut w);
        assert_eq!(
            w.stroke, night.overlay,
            "a palette swap must carry the overprint to the NEW palette's overprint, not \
             to #{:02X}{:02X}{:02X} (road_generic)",
            night.road_generic.r(), night.road_generic.g(), night.road_generic.b(),
        );
        assert_ne!(w.stroke, night.road_generic, "…and above all NEVER to road_generic");

        // …and the theme-derived palette carries it too, so a themed host is not left
        // with an overlay that is one of its own road tiers.
        let th = MapPalette::from_theme(&Theme::default());
        for (what, c) in [
            ("road_generic", th.road_generic),
            ("road_motorway", th.road_motorway),
            ("road_secondary", th.road_secondary),
            ("land", th.land),
        ] {
            assert!(
                channel_distance(th.overlay, c) >= 120,
                "the THEMED overlay ink #{:02X}{:02X}{:02X} is not tellable from {what} \
                 #{:02X}{:02X}{:02X}",
                th.overlay.r(), th.overlay.g(), th.overlay.b(), c.r(), c.g(), c.b(),
            );
        }
    }

    #[test]
    fn a_track_is_a_vehicular_road_not_a_footpath() {
        let pal = MapPalette::cartographic(true);
        let sty = |kv: &[(&str, &str)]| classify_with(&tags(kv), &pal);

        let track = sty(&[("highway", "track")]);
        let service = sty(&[("highway", "service")]);
        let footway = sty(&[("highway", "footway")]);

        // ── IT IS A ROAD, and specifically the same kind of road `service` is.
        assert_eq!(track.kind, WayKind::Road, "a track is a road you can drive");
        assert_eq!(track.color, service.color, "a track paints in road ink, like `service`");
        assert_eq!(track.width, service.width, "…at road width, like `service`");
        // …and since `railtier-2`, on the same TIER as well. The two are korp's own z14
        // rung ("the minor VEHICULAR tail") and were identical in ink, width and kind
        // while differing only in tier — `service` at City, `track` at Detail — which was
        // the inconsistency rather than the design. `service` promised two zooms korp
        // does not serve; it joined `track` here instead.
        assert_eq!(track.lod, service.lod, "…and on the same rung: one korp tier, one tier here");

        // ── AND IT IS NOT A FOOTPATH. Stated against the arm it came out of, so the
        // revert is what fails rather than something downstream of it.
        assert_ne!(track.kind, footway.kind, "a track must not classify as WayKind::Path");
        assert_ne!(track.color, footway.color, "a track must not paint in the footpath ink");
        assert_ne!(track.width, footway.width, "…nor at the footpath's width");

        // ── AND THE TIER DID NOT MOVE. The fix is a classification change, not a
        // ladder change; see the doc above for why 15 and not korp's 14.
        assert_eq!(track.lod, Lod::Detail, "the tier stays at Detail — facett draws from 15");
        assert_eq!(footway.lod, Lod::Detail, "…the same tier the footpath keeps");
        assert_eq!(
            crate::layer::LOD_TIER_ZOOMS[Lod::Detail as usize],
            15,
            "…which is camera 15, one zoom after korp starts sending tracks at 14"
        );
    }

    /// The label classifier extracts a `ref`/`name` and tiers its LOD so labels
    /// declutter by zoom (country places far out, street names only zoomed in) —
    /// the text half of the Garmin-feel lever. RED if a tier ever collapses or a
    /// `ref` stops winning over a co-tagged `name`.
    #[test]
    fn classify_label_extracts_name_ref_and_tiers_lod() {
        // Settlement ranks, one per rung of LOD_TIER_ZOOMS = [0, 6, 9, 12, 15]:
        //   country → Country(0) · city/town → Region(6) · village → District(9)
        //   · hamlet and the rest → City(12)
        // The District line CHANGED in `label-district-1` and this assertion changed with
        // it deliberately: no settlement class used to resolve to District at all, so the
        // ladder skipped from 6 straight to 12 and named nothing new across six zoom
        // levels — measured on the live client as 5 319 labels fed and 1 drawn at camera
        // 11.3, against a pane budget of ~65. `classify_label`'s own doc carries the
        // density measurement that decides which class earns the rung.
        let country = classify_label(&tags(&[("place", "country"), ("name", "Sverige")])).unwrap();
        assert_eq!(country, LabelClass { text: "Sverige".into(), lod: Lod::Country, rank: 1.0 });
        // `city` is COUNTRY tier, not Region (`label-city-country-1`): there are six
        // named `place=city` in the whole 2026-08-31 Sweden cut against one
        // `place=country`, and a whole-country pane that opened no rung but Country drew
        // no names at all (measured live at camera 5.6: 15 797 fed / 0 drawn).
        let city = classify_label(&tags(&[("place", "city"), ("name", "Stockholm")])).unwrap();
        assert_eq!(city.lod, Lod::Country, "a city carries the country view");
        // `town` is the class Region is FOR — 67 of them nationally.
        let town = classify_label(&tags(&[("place", "town"), ("name", "Sigtuna")])).unwrap();
        assert_eq!(town.lod, Lod::Region, "a town shows at the regional tier");
        let village = classify_label(&tags(&[("place", "village"), ("name", "Grönhögen")])).unwrap();
        assert_eq!(
            village.lod,
            Lod::District,
            "a village stands on the District rung — 996 named villages in the whole \
             country is regional density"
        );
        // The OTHER side of the same decision, asserted here so the two cannot drift: the
        // dense classes stay at City. 14 348 named hamlets and 25 221 isolated dwellings
        // against those 996 villages, measured on the 2026-08-31 Sweden cut.
        let hamlet = classify_label(&tags(&[("place", "hamlet"), ("name", "Vreta")])).unwrap();
        assert_eq!(hamlet.lod, Lod::City, "a hamlet is not a regional fact");
        let dwelling =
            classify_label(&tags(&[("place", "isolated_dwelling"), ("name", "Norrgården")])).unwrap();
        assert_eq!(dwelling.lod, Lod::City, "nor is a farmstead");
        assert!(
            village.rank > hamlet.rank && city.rank > village.rank,
            "the ranks must order the rungs too — city {} > village {} > hamlet {}",
            city.rank,
            village.rank,
            hamlet.rank,
        );

        // A road ref (shield) wins over its street name and shows regionally.
        let ref_road = classify_label(&tags(&[("highway", "motorway"), ("ref", "E4"), ("name", "Essingeleden")])).unwrap();
        assert_eq!(ref_road, LabelClass { text: "E4".into(), lod: Lod::Region, rank: 0.95 }, "the E4 shield wins over the street name");
        // A named street with no ref → its name at city LOD.
        let street = classify_label(&tags(&[("highway", "residential"), ("name", "Drottninggatan")])).unwrap();
        assert_eq!(street, LabelClass { text: "Drottninggatan".into(), lod: Lod::City, rank: label_rank_of_style(&classify_with(&tags(&[("highway", "residential")]), &MapPalette::cartographic(true))) });

        // A named lake → its name, zoomed-in only. Unnamed / untagged → no label.
        let lake = classify_label(&tags(&[("natural", "water"), ("name", "Mälaren")])).unwrap();
        assert_eq!(lake, LabelClass { text: "Mälaren".into(), lod: Lod::City, rank: 0.55 });
        assert!(classify_label(&tags(&[("highway", "service")])).is_none(), "an unnamed road has no label");
        assert!(classify_label(&tags(&[("building", "yes")])).is_none(), "an untagged building has no label");

        #[cfg(feature = "testmatrix")]
        facett_core::testmatrix::emit(
            "facett-map",
            "classify_label",
            country.lod == Lod::Country && city.lod == Lod::Region && ref_road.text == "E4",
            "label classifier extracts name/ref and tiers country→region→city LOD",
        );
    }

    #[test]
    fn water_and_landuse_are_areas_with_fill_roads_are_lines() {
        let th = Theme::default();
        let water = classify(&tags(&[("natural", "water")]), &th);
        assert!(water.is_area, "natural=water is an area");
        assert!(water.fill.is_some(), "an area carries a fill colour");
        assert_eq!(water.kind, WayKind::Water);

        let park = classify(&tags(&[("leisure", "park")]), &th);
        assert!(park.is_area && park.fill.is_some(), "leisure=park is a filled area");
        assert_eq!(park.kind, WayKind::Park);

        let landuse = classify(&tags(&[("landuse", "forest")]), &th);
        assert!(landuse.is_area && landuse.fill.is_some(), "landuse=forest is a filled area");

        let building = classify(&tags(&[("building", "yes")]), &th);
        assert!(building.is_area && building.fill.is_some(), "a building is a filled area");
        assert_eq!(building.kind, WayKind::Building);

        // Roads + coastline are LINES (stroke only, no fill).
        let road = classify(&tags(&[("highway", "motorway")]), &th);
        assert!(!road.is_area && road.fill.is_none(), "a road is a line, never filled");
        assert_eq!(road.kind, WayKind::Road);
        let coast = classify(&tags(&[("natural", "coastline")]), &th);
        assert!(!coast.is_area && coast.fill.is_none(), "the coastline is a line");
        assert_eq!(coast.kind, WayKind::Coastline);
    }

    #[test]
    fn road_class_drives_width_so_motorways_are_thicker_than_residential() {
        let th = Theme::default();
        let motorway = classify(&tags(&[("highway", "motorway")]), &th);
        let primary = classify(&tags(&[("highway", "primary")]), &th);
        let residential = classify(&tags(&[("highway", "residential")]), &th);
        assert!(motorway.width > primary.width, "motorway wider than primary");
        assert!(primary.width > residential.width, "primary wider than residential");
        assert_eq!(motorway.color, th.accent, "motorways ride the warm accent");
    }

    #[test]
    fn map_palette_distinguishes_water_green_land() {
        let pal = MapPalette::from_theme(&Theme::default());
        assert_ne!(pal.water, pal.green, "water and green fills differ");
        assert_ne!(pal.land, pal.water, "land background differs from water");
        // water is bluer than it is green; green is greener than it is blue.
        assert!(pal.water.b() > pal.water.g(), "water leans blue");
        assert!(pal.green.g() >= pal.green.b(), "green leans green");
    }

    // ── cartographic preset (the beautify delta) ────────────────────────────

    /// The palette-swappable classifier reproduces the themed `classify` exactly
    /// when fed `from_theme` — the refactor is a pure hoist, no visual drift
    /// (red if a road/area colour or width ever diverges).
    #[test]
    fn classify_with_from_theme_matches_classify_byte_for_byte() {
        let th = Theme::default();
        let pal = MapPalette::from_theme(&th);
        for kv in [
            vec![("highway", "motorway")],
            vec![("highway", "primary")],
            vec![("highway", "secondary")],
            vec![("highway", "tertiary")],
            vec![("highway", "residential")],
            vec![("highway", "footway")],
            vec![("highway", "raceway")], // the `_` generic arm
            vec![("natural", "water")],
            vec![("waterway", "riverbank")],
            vec![("leisure", "park")],
            vec![("landuse", "forest")],
            vec![("building", "yes")],
            vec![("natural", "coastline")],
            vec![("boundary", "administrative")], // untagged-for-us generic line
        ] {
            let a = classify(&tags(&kv), &th);
            let b = classify_with(&tags(&kv), &pal);
            assert_eq!(a.color, b.color, "stroke colour matches for {kv:?}");
            assert_eq!(a.fill, b.fill, "fill matches for {kv:?}");
            assert_eq!(a.width, b.width, "width matches for {kv:?}");
            assert_eq!(a.kind, b.kind, "kind matches for {kv:?}");
            assert_eq!(a.is_area, b.is_area, "is_area matches for {kv:?}");
        }
    }

    /// The cartographic preset is concrete hex (independent of the theme) and
    /// distinguishes day from night + water/green/land — red if a preset ever
    /// collapses two feature classes to the same colour.
    #[test]
    fn cartographic_preset_is_concrete_and_day_differs_from_night() {
        let day = MapPalette::cartographic(true);
        let night = MapPalette::cartographic(false);
        // A theme swap must NOT move the cartographic colours.
        assert_eq!(day.water, MapPalette::cartographic(true).water, "cartographic is a fixed preset");
        // Day is a light paper; night is a dark basemap.
        let sum = |c: Color32| c.r() as u16 + c.g() as u16 + c.b() as u16;
        assert!(sum(day.land) > 600, "day land is light");
        assert!(sum(night.land) < 150, "night land is dark");
        // Feature classes stay distinct in both.
        for p in [day, night] {
            assert_ne!(p.water, p.green, "water ≠ green");
            assert_ne!(p.land, p.water, "land ≠ water");
            assert_ne!(p.road_motorway, p.road_residential, "top road tier ≠ minor");
            assert!(p.water.b() > p.water.g(), "water leans blue");
        }
        // Road tiers ramp from a hot/bright top to a recessed minor.
        assert_ne!(day.road_motorway, night.road_motorway, "day ≠ night motorway hue");
    }

    /// The road-class rank ramps monotonically with the classifier's widths, so a
    /// motorway ranks above a primary above a residential — the z-order the
    /// Garmin-nav painter sorts on. Red if a tier's width threshold ever drifts out
    /// of step with [`road_rank`].
    #[test]
    fn road_rank_ramps_with_class_width() {
        let th = Theme::default();
        let r = |hw: &str| road_rank(classify(&tags(&[("highway", hw)]), &th).width);
        assert!(r("motorway") > r("primary"), "motorway ranks above primary");
        assert!(r("primary") > r("secondary"), "primary above secondary");
        assert!(r("secondary") > r("tertiary"), "secondary above tertiary");
        assert!(r("tertiary") > r("residential"), "tertiary above residential");
        assert_eq!(r("motorway"), 4, "motorway is the top rank");
        assert_eq!(r("residential"), 0, "residential is the bottom rank");
    }

    /// `recolor_way` swaps an already-built way to a new palette keyed on kind +
    /// width, keeping a filled ring filled and a line unfilled — red if a
    /// runtime palette switch ever loses a fill or mis-hues a road tier.
    #[test]
    fn recolor_way_reskins_by_kind_and_keeps_fill_state() {
        use crate::Way;
        let th = Theme::default();
        // A filled water ring.
        let ring = [(9.50, 47.10), (9.52, 47.10), (9.52, 47.12), (9.50, 47.12), (9.50, 47.10)];
        let mut water = Way::from_lonlat(1, &ring, &tags(&[("natural", "water")]), &th).unwrap();
        assert!(water.fill.is_some(), "water ring starts filled");
        // A motorway line.
        let mut motorway =
            Way::from_lonlat(2, &[(9.50, 47.10), (9.60, 47.20)], &tags(&[("highway", "motorway")]), &th).unwrap();

        let pal = MapPalette::cartographic(false);
        pal.recolor_way(&mut water);
        pal.recolor_way(&mut motorway);

        assert_eq!(water.fill, Some(pal.water), "water recoloured to the preset water, still filled");
        assert!(motorway.fill.is_none(), "the motorway line stays unfilled");
        assert_eq!(motorway.stroke, pal.road_motorway, "the widest road takes the top tier hue");
    }
}

/// **THE CROSS-REPO TAXONOMY GUARD — `facett` vs `osm-katana`, on real Swedish tags.**
///
/// # The defect this exists for
///
/// `osm_katana::shared::class::RULES` is an **arm-for-arm transcription** of the two
/// functions above — `classify_with` for the geometry classes, [`classify_label`] for
/// the `place=*` settlement backbone. Its own module doc says so. But *a transcription
/// is not a construction*: nothing makes the two texts stay in step, they live in two
/// repos, and the drift is silent in both directions — facett paints a feature at a rung
/// the server does not send it at, or the server sends a rung facett will not paint.
///
/// This constellation has paid for that shape twice already. `facett-geomap`'s
/// `osm2d.rs` was a complete SECOND 2D OSM painter over the same `Way` type, with its
/// own band ladder, and it took days to find. korp `b937078` fixed a bug whose entire
/// cause was points and ways going through different tag → class maps: a sub-pixel lake
/// became an off-by-default grey dot in a layer that was off by default.
///
/// **The by-construction fix is out of this guard's scope** — lift the kind+tier half of
/// `classify_with` into `edda` and have both sides call the one function. That spans
/// three repos and a crates.io release cascade and is recorded as DELTA 1's migration
/// target in `znippy-zoomies/.nornir/osm-katana-v2-design.md`. Until it happens this is
/// the detector, and it is written so the drift is impossible to MISS rather than
/// impossible to make.
///
/// # How facett reaches osm-katana's rules without depending on osm-katana
///
/// It cannot, and it must not: `facett-map` is an `egui` component crate and
/// `osm-katana` is a headless PBF converter that pulls in `parquet` and `gatling`. So
/// the ladder arrives as a **checked-in fixture**, `facett-map/osm-katana-ladder.txt`,
/// which is the verbatim stdout of one named command:
///
/// ```text
/// cargo run -p osm-katana --example ladder_spelling \
///     > ../facett/facett-map/osm-katana-ladder.txt
/// ```
///
/// Saying that out loud matters, because a fixture is a copy and a copy can go stale.
/// **Staleness is detectable, and it is detectable on the side that causes it**:
/// `osm-katana/tests/facett_ladder_fixture.rs` pins that command's output against a
/// literal, so editing `RULES` turns a test red *in the repo where the edit happened*,
/// with this file's path and the regeneration command in the failure message. facett
/// cannot see into another repo; that test is what makes the other side notice.
///
/// What this fixture is NOT: a constant copied out of osm-katana by hand. The parser
/// below reads the rule TESTS out of the file and evaluates them, so the fixture is
/// executable text, not a table of expected answers. An assertion against a table of
/// expected answers would be red for a facett change and blind to an osm-katana one.
///
/// # The corpus is real, and it is weighted
///
/// [`CORPUS`] is the 110 most frequent distinct tag signatures of the live Sweden cut
/// (`~/.korp/work/sweden`, 2026-08-31), over the keys either classifier reads, taken
/// from `ways.parquet` (9 325 142 rows) and `nodes.parquet` (103 488 583 rows), plus
/// (2026-09-01) the seven `boundary` / `admin_level` way signatures with rows. The
/// counts are that census. A hand-written corpus would have found the arms someone
/// thought of; this one found the arms Sweden actually has, and that is how the
/// `place=islet` coastline entry below was discovered rather than imagined.
///
/// # Two halves, because facett has two functions
///
/// | half | facett | what it decides |
/// |---|---|---|
/// | way | `classify_with(tags).lod` | the rung a WAY is painted at |
/// | label | `classify_label(tags)?.lod` | the rung a settlement NAME is placed at |
///
/// `RULES` is the union of the two, so both are checked against it. The way half is
/// where the known `place` hole shows: `classify_with` has **no `place` arm at all**.
#[cfg(test)]
mod osm_katana_ladder {
    use super::*;

    /// The generated ladder. `include_str!` and not a `const` transcription: the
    /// bytes in the file are the bytes the example printed.
    const LADDER: &str = include_str!("../osm-katana-ladder.txt");

    /// One rule out of the fixture: a tag test and the rung it grants.
    #[derive(Debug)]
    struct Rule {
        key: String,
        kind: Kind,
        rung: u8,
    }

    #[derive(Debug)]
    enum Kind {
        /// `key∈[a|b|c]`
        ValueIn(Vec<String>),
        /// `key?`
        Present,
        /// `key?!guard∈[a|b]`
        PresentUnless { guard: String, values: Vec<String> },
    }

    impl Rule {
        fn matches<K: AsRef<str>, V: AsRef<str>>(&self, tags: &[(K, V)]) -> bool {
            let v = crate::tag(tags, &self.key);
            match &self.kind {
                Kind::ValueIn(vals) => v.is_some_and(|v| vals.iter().any(|x| x == v)),
                Kind::Present => v.is_some(),
                Kind::PresentUnless { guard, values } => {
                    v.is_some()
                        && !crate::tag(tags, guard).is_some_and(|g| values.iter().any(|x| x == g))
                }
            }
        }
    }

    /// The parsed fixture: the rungs, the unclassified rung, and the rules in order.
    struct Ladder {
        rungs: Vec<u8>,
        unclassified: u8,
        rules: Vec<Rule>,
    }

    fn parse() -> Ladder {
        let mut rungs = Vec::new();
        let mut unclassified = None;
        let mut rules = Vec::new();
        let mut in_rules = false;
        for line in LADDER.lines() {
            let line = line.trim();
            if line.is_empty() {
                continue;
            }
            if let Some(r) = line.strip_prefix("# rungs:") {
                rungs = r
                    .trim()
                    .trim_start_matches('[')
                    .trim_end_matches(']')
                    .split(',')
                    .map(|s| s.trim().parse::<u8>().expect("a rung is a number"))
                    .collect();
                continue;
            }
            if let Some(u) = line.strip_prefix("# unclassified:") {
                unclassified = Some(u.trim().parse::<u8>().expect("a rung is a number"));
                continue;
            }
            if line == "# rules" {
                in_rules = true;
                continue;
            }
            if line.starts_with('#') || !in_rules {
                continue;
            }
            let (test, rung) = line.rsplit_once('>').unwrap_or_else(|| {
                panic!("fixture rule `{line}` has no `>rung` — regenerate it")
            });
            let rung: u8 = rung.parse().expect("a rung is a number");
            let list = |s: &str| -> Vec<String> {
                s.trim_start_matches('[')
                    .trim_end_matches(']')
                    .split('|')
                    .map(str::to_string)
                    .collect()
            };
            let r = if let Some((key, rest)) = test.split_once("?!") {
                let (guard, vals) = rest.split_once('∈').expect("a guard has a value list");
                Rule {
                    key: key.to_string(),
                    kind: Kind::PresentUnless { guard: guard.to_string(), values: list(vals) },
                    rung,
                }
            } else if let Some((key, vals)) = test.split_once('∈') {
                Rule { key: key.to_string(), kind: Kind::ValueIn(list(vals)), rung }
            } else {
                Rule { key: test.trim_end_matches('?').to_string(), kind: Kind::Present, rung }
            };
            rules.push(r);
        }
        Ladder {
            rungs,
            unclassified: unclassified.expect("the fixture must name its unclassified rung"),
            rules,
        }
    }

    impl Ladder {
        /// The rung osm-katana would write for `tags` — first matching rule wins.
        fn rung_of<K: AsRef<str>, V: AsRef<str>>(&self, tags: &[(K, V)]) -> u8 {
            self.rules
                .iter()
                .find(|r| r.matches(tags))
                .map_or(self.unclassified, |r| r.rung)
        }

        /// The coarsest rung at or above `camera` — osm-katana's `class::project`,
        /// which is the projection of `LOD_TIER_ZOOMS` onto the fixture's own rungs.
        fn project(&self, camera: u8) -> u8 {
            self.rungs
                .iter()
                .copied()
                .find(|&r| r >= camera)
                .or_else(|| self.rungs.last().copied())
                .unwrap_or(camera)
        }

        fn rung_of_lod(&self, lod: Lod) -> u8 {
            self.project(crate::layer::LOD_TIER_ZOOMS[lod as usize])
        }
    }

    fn owned(kv: &[(&str, &str)]) -> Vec<(String, String)> {
        kv.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
    }

    /// The rung facett PAINTS a way of these tags at.
    fn facett_way_rung(l: &Ladder, kv: &[(&str, &str)]) -> u8 {
        l.rung_of_lod(classify_with(&owned(kv), &MapPalette::cartographic(true)).lod)
    }

    /// The rung facett places the NAME of a feature of these tags at, or `None` when
    /// it would not label it at all.
    fn facett_label_rung(l: &Ladder, kv: &[(&str, &str)]) -> Option<u8> {
        let mut with_name = owned(kv);
        with_name.push((String::from("name"), String::from("Namnet")));
        classify_label(&with_name).map(|c| l.rung_of_lod(c.lod))
    }

    // ── the corpus ──────────────────────────────────────────────────────────────

    /// **The 110 most frequent distinct tag signatures of the live Sweden cut**, with
    /// the row count each was measured at (`~/.korp/work/sweden`, 2026-08-31; 55 from
    /// `ways.parquet` / 9 325 142 rows, 55 from `nodes.parquet` / 103 488 583 rows),
    /// restricted to the keys either classifier reads. Plus the tiering-relevant tags
    /// the census does not reach in volume but the ladder is *about* — the arterials,
    /// the settlement classes above `village`, the marked routes.
    ///
    /// The count is carried so a divergence can be reported in ROWS and not only in
    /// arms: "facett and the server disagree" is an argument, "they disagree about
    /// 2 603 484 buildings" is a fact.
    const CORPUS: &[(&[(&str, &str)], u64)] = &[
    (&[("building", "yes")], 2603484),
    (&[("highway", "service")], 604496),
    (&[("building", "house")], 425590),
    (&[("highway", "track")], 309149),
    (&[("landuse", "forest")], 286345),
    (&[("highway", "path")], 276399),
    (&[("highway", "footway")], 269590),
    (&[("highway", "residential")], 231580),
    (&[("highway", "unclassified")], 200071),
    (&[("amenity", "parking_space")], 186010),
    (&[("natural", "wetland")], 176094),
    (&[("building", "detached")], 166146),
    (&[("landuse", "farmland")], 148175),
    (&[("building", "garage")], 133589),
    (&[("waterway", "stream")], 124643),
    (&[("building", "residential")], 115782),
    (&[("landuse", "residential")], 97238),
    (&[("highway", "tertiary")], 92622),
    (&[("amenity", "parking")], 90086),
    (&[("natural", "water"), ("water", "lake")], 87196),
    (&[("landuse", "grass")], 86649),
    (&[("highway", "cycleway")], 79401),
    (&[("natural", "grassland")], 75953),
    (&[("natural", "heath")], 74384),
    (&[("building", "apartments")], 74293),
    (&[("landuse", "meadow")], 69686),
    (&[("building", "shed")], 64104),
    (&[("landuse", "forest"), ("natural", "wetland")], 63659),
    (&[("barrier", "hedge")], 60408),
    (&[("natural", "coastline")], 46926),
    (&[("natural", "wood")], 45384),
    (&[("natural", "scrub")], 45264),
    (&[("landuse", "forest"), ("natural", "scrub")], 44998),
    (&[("highway", "secondary")], 44033),
    (&[("man_made", "pier")], 40032),
    (&[("building", "terrace")], 39588),
    (&[("building", "roof")], 33839),
    (&[("natural", "water")], 32997),
    (&[("barrier", "fence")], 32746),
    (&[("natural", "coastline"), ("place", "islet")], 31112),
    (&[("waterway", "ditch")], 30363),
    (&[("landuse", "farmyard")], 27959),
    (&[("railway", "rail")], 23426),
    (&[("highway", "platform")], 21712),
    (&[("highway", "steps")], 21112),
    (&[("building", "garages")], 20955),
    (&[("leisure", "pitch")], 20370),
    (&[("natural", "bare_rock")], 19092),
    (&[("building", "industrial")], 18823),
    (&[("highway", "trunk")], 18434),
    (&[("power", "generator")], 17648),
    (&[("barrier", "wall")], 17201),
    (&[("highway", "primary")], 17112),
    (&[("waterway", "river")], 16299),
    (&[("leisure", "playground")], 15380),
    (&[("natural", "tree")], 414512),
    (&[("highway", "street_lamp")], 213315),
    (&[("power", "tower")], 113341),
    (&[("place", "isolated_dwelling")], 98938),
    (&[("highway", "turning_circle")], 92756),
    (&[("highway", "crossing")], 83805),
    (&[("power", "pole")], 65605),
    (&[("highway", "bus_stop")], 57425),
    (&[("place", "hamlet")], 39603),
    (&[("barrier", "gate")], 35724),
    (&[("amenity", "bench")], 33141),
    (&[("highway", "give_way")], 28044),
    (&[("railway", "signal")], 18884),
    (&[("railway", "milestone")], 16180),
    (&[("amenity", "waste_basket")], 16008),
    (&[("natural", "peak")], 15188),
    (&[("place", "locality")], 15029),
    (&[("man_made", "flagpole")], 13047),
    (&[("leisure", "picnic_table")], 11226),
    (&[("amenity", "shelter")], 10324),
    (&[("barrier", "block")], 10091),
    (&[("amenity", "restaurant")], 10016),
    (&[("railway", "level_crossing")], 9197),
    (&[("railway", "switch")], 9174),
    (&[("highway", "stop")], 9108),
    (&[("amenity", "bicycle_parking")], 9072),
    (&[("place", "farm")], 9011),
    (&[("highway", "traffic_signals")], 6937),
    (&[("barrier", "lift_gate")], 6613),
    (&[("amenity", "toilets")], 5608),
    (&[("man_made", "mast")], 5416),
    (&[("natural", "bay")], 5198),
    (&[("amenity", "post_box")], 5078),
    (&[("power", "catenary_mast")], 4681),
    (&[("natural", "rock")], 4508),
    (&[("leisure", "firepit")], 4435),
    (&[("amenity", "recycling")], 4074),
    (&[("amenity", "charging_station")], 3699),
    (&[("barrier", "swing_gate")], 3673),
    (&[("amenity", "cafe")], 3632),
    (&[("railway", "crossing")], 3554),
    (&[("place", "neighbourhood")], 3400),
    (&[("man_made", "manhole")], 3298),
    (&[("amenity", "fast_food")], 3167),
    (&[("railway", "buffer_stop")], 2824),
    (&[("barrier", "cycle_barrier")], 2813),
    (&[("barrier", "bollard")], 2761),
    (&[("amenity", "bbq")], 2677),
    (&[("man_made", "surveillance")], 2651),
    (&[("place", "village")], 2570),
    (&[("amenity", "fuel")], 2514),
    (&[("leisure", "bathing_place")], 2486),
    (&[("highway", "speed_camera")], 2465),
    (&[("natural", "stone")], 2442),
    (&[("power", "switch")], 2341),
        // ── the tiering backbone the frequency census does not reach ──────────────
        (&[("highway", "motorway")], 0),
        (&[("highway", "motorway_link")], 0),
        (&[("highway", "primary_link")], 0),
        (&[("highway", "secondary_link")], 0),
        (&[("highway", "tertiary_link")], 0),
        (&[("highway", "living_street")], 0),
        (&[("highway", "pedestrian")], 0),
        (&[("highway", "road")], 0),
        (&[("highway", "busway")], 0),
        (&[("highway", "raceway")], 0),
        (&[("railway", "narrow_gauge")], 0),
        (&[("railway", "tram")], 0),
        (&[("railway", "subway")], 0),
        (&[("waterway", "canal")], 0),
        (&[("waterway", "riverbank")], 0),
        (&[("waterway", "drain")], 0),
        (&[("natural", "forest")], 0),
        (&[("leisure", "park")], 0),
        (&[("leisure", "nature_reserve")], 0),
        (&[("landuse", "industrial")], 0),
        (&[("place", "country")], 0),
        (&[("place", "region")], 0),
        (&[("place", "city")], 0),
        (&[("place", "town")], 0),
        (&[("place", "municipality")], 0),
        (&[("place", "suburb")], 0),
        // ── the `boundary` block (osm-katana `0ac5dfa`, 2026-09-01) ──────────────
        //
        // MEASURED on `~/.korp/work/sweden/unpacked-backup/ways.parquet` (2026-09-01,
        // 9 325 142 rows): 4 601 ways carry `boundary` or `admin_level` at all, and
        // these are the seven signatures with more than a handful of rows. The
        // nodes table has 57 such rows in 103 488 583 and is not sampled. The
        // volume is in RELATIONS (717 019 `boundary` rows, per osm-katana's census),
        // which neither classifier is asked about here — this corpus is ways.
        (&[("boundary", "administrative"), ("admin_level", "7")], 1893),
        (&[("boundary", "protected_area"), ("leisure", "nature_reserve")], 1384),
        (&[("boundary", "protected_area")], 464),
        (&[("boundary", "administrative"), ("admin_level", "4")], 353),
        (&[("boundary", "administrative"), ("admin_level", "2")], 113),
        (&[("boundary", "administrative"), ("admin_level", "9")], 54),
        (&[("boundary", "national_park")], 9),
        // Marked routes, and the arterial guard that must survive them.
        (&[("route", "hiking"), ("highway", "path")], 0),
        (&[("route", "bicycle"), ("highway", "primary")], 0),
        (&[("piste:type", "downhill")], 0),
        (&[("sac_scale", "hiking"), ("highway", "path")], 0),
        (&[("mtb:scale", "2"), ("highway", "path")], 0),
        (&[("snowmobile", "designated"), ("highway", "track")], 0),
    ];

    /// One [`KNOWN_DIVERGENCES`] entry: `(tags, facett rung, osm-katana rung, half, why)`.
    type Divergence = (&'static [(&'static str, &'static str)], u8, u8, &'static str, &'static str);

    /// **The divergences that are LIVE, named, and deliberately not fixed here.**
    ///
    /// `(tags, facett rung, osm-katana rung, half, why)`. This is not a mute
    /// suppression list: [`the_two_taxonomies_agree_on_real_swedish_tags`] fails when a
    /// listed pair no longer diverges, or diverges by different rungs, exactly as it
    /// fails on an unlisted one. Closing a hole therefore forces its entry out, and a
    /// rung that shifts underneath one is reported rather than absorbed.
    ///
    /// Every entry here is a REPORT, not a decision. Changing what facett draws changes
    /// pixels and belongs to a lane that can measure them.
    const KNOWN_DIVERGENCES: &[Divergence] = &[
        // ── THE `place` HOLE — six of the seven, and all one defect ───────────────
        //
        // `classify_with` has **no `place` arm at all**. A `place=city` NODE goes
        // through `classify_label` and earns `Lod::Country`; a `place=city` closed WAY
        // goes through `classify_with`, matches nothing, and lands on the
        // `_ => Lod::City` fallthrough. Same tags, two geometries, two rungs — the
        // exact shape of `korp-map-pt-1` (2026-08-31), where points and ways went
        // through different tag → class maps and a sub-pixel lake became an
        // off-by-default grey dot in a layer that was off by default.
        //
        // osm-katana closed it by putting the `place=*` block in ONE table both
        // geometries read. facett still has the hole, and closing it here would change
        // what the painter draws — which belongs to a lane that can measure pixels, not
        // to the lane that built the detector.
        //
        // How much it costs TODAY, measured on the 2026-08-31 Sweden cut: almost
        // nothing, and that is worth writing down rather than leaving as an assumption.
        // Swedish settlements are mapped as NODES and as boundary relations, not as
        // closed ways: over the 400 most frequent way signatures there is not one
        // `place=city`, `place=town` or `place=village` way. The hole is real, it is
        // one rung to six rungs deep, and Sweden happens not to walk into it. Another
        // country's cut is not promised to be as lucky.
        (&[("place", "country")], 12, 6, "way", "classify_with has no place arm"),
        (&[("place", "region")], 12, 6, "way", "classify_with has no place arm"),
        (&[("place", "city")], 12, 6, "way", "classify_with has no place arm"),
        (&[("place", "town")], 12, 6, "way", "classify_with has no place arm"),
        (&[("place", "municipality")], 12, 9, "way", "classify_with has no place arm"),
        (&[("place", "village")], 12, 9, "way", "classify_with has no place arm"),
        // ── THE ISLET, FROM THE OTHER SIDE — closed on the way half, open on the label ─
        //
        // Until osm-katana `islet-1` (2026-09-01) this list carried a WAY entry here:
        // `RULES` asked its `place?` catch-all before `natural=coastline`, so an islet's
        // shoreline (`natural=coastline` + `place=islet`, **31 112** Swedish ways) was
        // written `min_zoom = 12` while facett paints coastline at rung 6. osm-katana
        // moved `natural=coastline` to the head of the ladder; the regenerated fixture
        // carries that order and the two sides now AGREE on 6 — the guard reported the
        // closed hole by name and the entry is gone, exactly as its doc promised.
        //
        // What the reorder exposed is the LABEL half of the same rows: `classify_label`
        // places the NAME of any `place=islet` at `Lod::City` (rung 12) — every islet,
        // island and locality name is City there by design, they are not settlements —
        // while the FEATURE is now served from rung 6. A camera at 6–11 gets the
        // shoreline and no name, which is the harmless direction (a client withholding
        // what the server sends). Reported, not fixed: where an islet's name appears is
        // a label-placement decision with pixels behind it.
        (
            &[("natural", "coastline"), ("place", "islet")],
            12,
            6,
            "label",
            "classify_label places every place=islet name at City; the feature itself \
             now rides the coastline's rung 6",
        ),
        // ── THE `boundary` BLOCK — osm-katana grew arms facett does not have ───────
        //
        // osm-katana `0ac5dfa` (2026-09-01) gave a `boundary=*` line a rung of its own:
        // `admin_level` 1–2 and 3–4 at 6, 5–7 at 9, any other `admin_level` at 12,
        // `boundary=national_park|protected_area` at 6, any other `boundary` at 12.
        // `classify_with` has **no `boundary` arm at all** — a boundary way is the
        // "untagged-for-us generic line" its own byte-for-byte test names, and lands on
        // the `_ => Lod::City` fallthrough (rung 12). Two signatures agree by accident
        // and are NOT listed: `admin_level=9` (12 on both sides, the ladder's
        // `admin_level?` catch-all) and `boundary=protected_area` + `leisure=nature_reserve`
        // (6 on both — facett's `leisure` arm reaches it first, osm-katana's `boundary`
        // arm does).
        //
        // How much it costs, MEASURED on the 2026-09-01 Sweden cut: 2 832 way rows in
        // these five signatures, out of 4 601 boundary-tagged ways in 9 325 142. The
        // direction is the dangerous one (the server serves a national border at 6,
        // the client paints it at 12 — a camera at 6–11 is sent bytes it will not
        // draw), and the volume is small because Swedish boundaries live in RELATIONS
        // (717 019 rows), which this corpus does not sample. Reported, not fixed:
        // painting a border at the country tier changes pixels.
        (&[("boundary", "administrative"), ("admin_level", "7")], 12, 9, "way", "classify_with has no boundary arm"),
        (&[("boundary", "protected_area")], 12, 6, "way", "classify_with has no boundary arm"),
        (&[("boundary", "administrative"), ("admin_level", "4")], 12, 6, "way", "classify_with has no boundary arm"),
        (&[("boundary", "administrative"), ("admin_level", "2")], 12, 6, "way", "classify_with has no boundary arm"),
        (&[("boundary", "national_park")], 12, 6, "way", "classify_with has no boundary arm"),
    ];

    fn spell(kv: &[(&str, &str)]) -> String {
        kv.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(" ")
    }

    fn known(kv: &[(&str, &str)], half: &str) -> Option<(u8, u8)> {
        KNOWN_DIVERGENCES
            .iter()
            .find(|(t, _, _, h, _)| *h == half && spell(t) == spell(kv))
            .map(|(_, f, k, _, _)| (*f, *k))
    }

    // ── the guard ───────────────────────────────────────────────────────────────

    /// ★ **RED-when-broken: facett's tiering drifting from osm-katana's ladder.**
    ///
    /// For every tag set in [`CORPUS`], the rung facett would paint (and, for a
    /// settlement, label) at must equal the rung the shared ladder puts it on — with the
    /// exceptions in [`KNOWN_DIVERGENCES`], each of which is checked to still be exactly
    /// the divergence it claims to be.
    ///
    /// The failure NAMES the tag set, both rungs, which half disagreed, and how many
    /// Swedish rows carry it. A guard that reports "differs" without saying which tag is
    /// a guard nobody will act on.
    ///
    /// **RED ARMS, run and seen:**
    ///
    /// * `"tertiary" | "tertiary_link" => Lod::District` in [`classify_with`] changed to
    ///   `Lod::City` ⇒ *"way: `highway=tertiary` (92 622 rows) — facett rung 12, the
    ///   osm-katana ladder rung 9"*.
    /// * the `place=city` entry deleted from [`KNOWN_DIVERGENCES`] ⇒ the live `place`
    ///   hole is reported by name, which is what gate 4 asked for.
    /// * a listed divergence "fixed" in the list but not in the code ⇒ *"is listed as
    ///   diverging 12 vs 6 but actually diverges 12 vs 9"*.
    #[test]
    fn the_two_taxonomies_agree_on_real_swedish_tags() {
        let l = parse();
        let mut bad: Vec<String> = Vec::new();
        let mut compared = 0usize;
        let mut diverging_rows = 0u64;
        let mut hit: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();

        for (kv, rows) in CORPUS {
            let katana = l.rung_of(&owned(kv));

            // ── the WAY half: what the painter draws this at ──────────────────────
            let f = facett_way_rung(&l, kv);
            compared += 1;
            match (f == katana, known(kv, "way")) {
                (true, None) => {}
                (true, Some((wf, wk))) => bad.push(format!(
                    "way: `{}` is listed in KNOWN_DIVERGENCES as {wf} vs {wk} but the two \
                     sides now AGREE on {f}. The hole is closed — delete the entry.",
                    spell(kv)
                )),
                (false, Some((wf, wk))) if wf == f && wk == katana => {
                    diverging_rows += rows;
                    hit.insert(format!("way:{}", spell(kv)));
                }
                (false, Some((wf, wk))) => bad.push(format!(
                    "way: `{}` is listed as diverging {wf} vs {wk} but actually diverges \
                     {f} vs {katana} — a rung moved underneath a known hole",
                    spell(kv)
                )),
                (false, None) => bad.push(format!(
                    "way: `{}` ({rows} Swedish rows) — facett paints it at rung {f}, the \
                     osm-katana ladder puts it on {katana}. One of the two texts moved. \
                     Fix the taxonomy, or record it in KNOWN_DIVERGENCES with its reason.",
                    spell(kv)
                )),
            }

            // ── the LABEL half ───────────────────────────────────────────────────
            //
            // Only for `place=*`. `RULES` is the union of TWO functions and it is
            // explicit about which arm came from which: the `place=*` block is
            // `classify_label`'s settlement backbone, everything else is
            // `classify_with`. `classify_label`'s other branches decide where a NAME
            // is placed, not what tier a feature IS — every named lake, park and
            // street resolves to `Lod::City` there by design — so comparing them
            // against the ladder would compare two different questions and report 30
            // divergences that are not drift.
            if crate::has_tag(&owned(kv), "place")
                && let Some(f) = facett_label_rung(&l, kv)
            {
                compared += 1;
                match (f == katana, known(kv, "label")) {
                    (true, None) => {}
                    (true, Some((wf, wk))) => bad.push(format!(
                        "label: `{}` is listed in KNOWN_DIVERGENCES as {wf} vs {wk} but \
                         the two sides now AGREE on {f} — delete the entry.",
                        spell(kv)
                    )),
                    (false, Some((wf, wk))) if wf == f && wk == katana => {
                        hit.insert(format!("label:{}", spell(kv)));
                    }
                    (false, Some((wf, wk))) => bad.push(format!(
                        "label: `{}` is listed as diverging {wf} vs {wk} but actually \
                         diverges {f} vs {katana}",
                        spell(kv)
                    )),
                    (false, None) => bad.push(format!(
                        "label: `{}` ({rows} Swedish rows) — facett places the name at \
                         rung {f}, the osm-katana ladder puts the feature on {katana}.",
                        spell(kv)
                    )),
                }
            }
        }

        // Every listed divergence must have been reached, or the list is describing tag
        // sets the corpus no longer holds and is quietly suppressing nothing.
        for (kv, _, _, half, _) in KNOWN_DIVERGENCES {
            let key = format!("{half}:{}", spell(kv));
            if !hit.contains(&key) {
                bad.push(format!(
                    "KNOWN_DIVERGENCES lists `{key}`, but the corpus never produced that \
                     comparison — the entry suppresses nothing and must go"
                ));
            }
        }

        assert!(
            bad.is_empty(),
            "facett and osm-katana are two TEXTS of one taxonomy and they have drifted \
             ({} of {compared} comparisons):\n  {}\n\nThe ladder facett is checked \
             against is `facett-map/osm-katana-ladder.txt`; regenerate it with `cargo \
             run -p osm-katana --example ladder_spelling`.",
            bad.len(),
            bad.join("\n  ")
        );

        println!(
            "{compared} comparisons over {} real Swedish tag signatures; {} known \
             divergences covering {diverging_rows} way rows",
            CORPUS.len(),
            KNOWN_DIVERGENCES.len()
        );
    }

    /// ★ **RED-when-broken: a guard that compares a value with a copy of itself.**
    ///
    /// The trap this codebase keeps hitting (`korp_tile_options().zooms ==
    /// DEFAULT_TILE_ZOOMS`, where the function returns that constant) has an exact shape
    /// here: if the fixture were parsed into *expected answers* rather than into rule
    /// TESTS, or if `rung_of` ignored its rules, every comparison above would be a
    /// constant compared with itself.
    ///
    /// Three arms, each of which a hollow instrument fails:
    ///
    /// 1. the fixture parses into a real ladder — rungs, an unclassified rung, and
    ///    enough rules to be the ladder rather than a fragment of it;
    /// 2. `rung_of` READS its rules: the same tags against a ladder whose rules have
    ///    been emptied must give a different answer;
    /// 3. the corpus lands on more than one rung on BOTH sides, so a collapsed ladder
    ///    on either side cannot look like agreement.
    #[test]
    fn the_guard_is_not_comparing_a_value_with_itself() {
        let l = parse();
        assert_eq!(l.rungs, vec![6u8, 9, 12, 15], "the fixture's rungs");
        assert_eq!(l.unclassified, 12, "the fixture's unclassified rung");
        assert!(
            l.rules.len() >= 30,
            "only {} rules parsed out of the fixture — it is a fragment, not the ladder",
            l.rules.len()
        );

        // Arm 2: the rules are actually consulted.
        let motorway = owned(&[("highway", "motorway")]);
        let empty = Ladder { rungs: l.rungs.clone(), unclassified: l.unclassified, rules: vec![] };
        assert_eq!(l.rung_of(&motorway), 6, "the real ladder tiers a motorway at 6");
        assert_ne!(
            l.rung_of(&motorway),
            empty.rung_of(&motorway),
            "`rung_of` gives the same answer with NO rules as with all of them — it is \
             not reading the fixture, and every comparison in this module is vacuous"
        );

        // Arm 3: both sides span rungs over the corpus.
        let mut katana: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
        let mut facett: std::collections::BTreeSet<u8> = std::collections::BTreeSet::new();
        for (kv, _) in CORPUS {
            katana.insert(l.rung_of(&owned(kv)));
            facett.insert(facett_way_rung(&l, kv));
        }
        assert!(
            katana.len() >= 4 && facett.len() >= 4,
            "the corpus lands on {katana:?} in osm-katana and {facett:?} in facett — a \
             side collapsed onto one rung would agree with itself everywhere"
        );
    }

    /// ★ **RED-when-broken: a fixture that is not the generated one.**
    ///
    /// The fixture is a copy, so the one thing that must be true of it is that it came
    /// out of the generator. Hand-editing it — the way a copy rots — loses the header,
    /// and a header-less fixture is a hand-written table of answers wearing a fixture's
    /// name.
    #[test]
    fn the_ladder_fixture_is_generated_and_says_so() {
        assert!(
            LADDER.contains("GENERATED, do not hand-edit"),
            "osm-katana-ladder.txt has lost its generated-file header"
        );
        assert!(
            LADDER.contains("cargo run -p osm-katana --example ladder_spelling"),
            "the fixture must carry the command that regenerates it, or the next person \
             hand-edits it"
        );
        assert!(
            LADDER.contains("# crate: osm-katana "),
            "the fixture must name the crate version it was taken from"
        );
    }
}