@chrtco/sdk 0.10.46 → 0.10.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { F as FileSystemAdapter } from './fs-adapter-D5iwdb1Q.cjs';
2
- import { C as ColorTableName } from './symbol-families-D2Kx20HJ.cjs';
3
- export { S as S52_COLORS, a as S52_DAY, b as S52_DUSK, c as S52_NIGHT, d as SYMBOL_FAMILIES, e as SYMBOL_FAMILY_META, f as SymbolFamily, g as SymbolFamilyMeta, h as getColorTable, i as iconExpressionForFamily, p as parseSymbolFamily, r as resolveColor } from './symbol-families-D2Kx20HJ.cjs';
2
+ import { C as ColorTableName } from './symbol-families-De7sN-p5.cjs';
3
+ export { E as EINK_COLOR_TABLES, a as EINK_MONO_GREYS, S as S52_COLORS, b as S52_DAY, c as S52_DUSK, d as S52_EINK_COLOR, e as S52_EINK_MONO, f as S52_EINK_NIGHT, g as S52_NIGHT, h as SYMBOL_FAMILIES, i as SYMBOL_FAMILY_META, j as SymbolFamily, k as SymbolFamilyMeta, l as getColorTable, m as iconExpressionForFamily, n as isEinkColorTable, o as isEinkGreyTable, p as parseSymbolFamily, r as resolveColor } from './symbol-families-De7sN-p5.cjs';
4
4
  import maplibregl from 'maplibre-gl';
5
5
  import { ExpressionSpecification } from '@maplibre/maplibre-gl-style-spec';
6
6
 
@@ -862,8 +862,10 @@ interface OfflineHeartbeatOptions {
862
862
  * from `GET /v1/offline/styles/{region}`.
863
863
  */
864
864
  interface OfflineFetchStyleOptions {
865
- /** S-52 colour palette. Default: `"DAY"`. */
866
- palette?: "DAY" | "DUSK" | "NIGHT";
865
+ /** Colour palette. Default: `"DAY"`. `"EINK_COLOR"` / `"EINK_MONO"` are
866
+ * the e-paper variants (colour and monochrome e-ink); `"EINK_NIGHT"` is
867
+ * e-ink at night. */
868
+ palette?: "DAY" | "DUSK" | "NIGHT" | "EINK_COLOR" | "EINK_MONO" | "EINK_NIGHT";
867
869
  /**
868
870
  * URL scheme baked into the source. Default: `"mbtiles"` (iOS/Android via
869
871
  * MLN Native). Use `"file"` for desktop (Win/macOS/Linux).
@@ -894,7 +896,7 @@ interface OfflineFetchStyleOptions {
894
896
  */
895
897
  interface OfflineStyleResponse {
896
898
  region: string;
897
- palette: "DAY" | "DUSK" | "NIGHT";
899
+ palette: "DAY" | "DUSK" | "NIGHT" | "EINK_COLOR" | "EINK_MONO" | "EINK_NIGHT";
898
900
  protocol: "mbtiles" | "file";
899
901
  /** Resolved symbol-artwork family (lowercased), echoed from `?symbols=`. */
900
902
  symbols?: ChrtSymbolSet;
@@ -1558,12 +1560,20 @@ declare class TileAuth {
1558
1560
 
1559
1561
  /** Symbol-artwork families served by the Style API. */
1560
1562
  type ChrtSymbolSet = "s52" | "s101" | "chrt" | "raster";
1561
- /** Palette themes served by the Style API (S-52 colour tables). */
1562
- type ChrtStyleTheme = "day" | "dusk" | "night";
1563
+ /**
1564
+ * Palette themes served by the Style API: the S-52 colour tables (`day`,
1565
+ * `dusk`, `night`) and CHRT's e-paper variants — `eink_color` for colour
1566
+ * e-ink (E Ink Kaleido 3) and `eink_mono` for monochrome e-ink panels and
1567
+ * monitors, `eink_night` for any e-ink screen at night (a black page). The
1568
+ * e-ink themes change portrayal as well as colour (heavier
1569
+ * lines, a heavy safety contour, depth steps by pattern in mono).
1570
+ */
1571
+ type ChrtStyleTheme = "day" | "dusk" | "night" | "eink_color" | "eink_mono" | "eink_night";
1563
1572
  interface StyleUrlOptions {
1564
- /** Palette: "day" (default), "dusk", or "night". Omit to get the style's
1565
- * default — for keys with a bound CHRTstyle custom style, that's the
1566
- * style's default palette. */
1573
+ /** Palette: "day" (default), "dusk", "night", "eink_color", "eink_mono"
1574
+ * or "eink_night". Omit to get the style's default — for keys with a bound
1575
+ * CHRTstyle custom style, that's the style's default palette (the e-ink
1576
+ * themes are always available, bound style or not). */
1567
1577
  theme?: ChrtStyleTheme;
1568
1578
  /** Symbol-artwork family: `s52` (default PresLib set), `s101` (official
1569
1579
  * S-101 Portrayal Catalogue symbols), `chrt` (the CHRT house set), or
@@ -2822,1321 +2832,1379 @@ declare class InteractClient {
2822
2832
  }
2823
2833
 
2824
2834
  /**
2825
- * Light arithmetic for the chart palettes: how much light a colour emits, and
2826
- * how to dim a colour without changing its hue.
2827
- *
2828
- * Everything here works in LINEAR light (WCAG 2.x relative luminance, sRGB
2829
- * primaries). That is the quantity a dark-adapted eye responds to, and it is
2830
- * the one `sounding-legibility.test.ts` already measures soundings in. Scaling
2831
- * the sRGB-ENCODED channels instead would dim dark colours far more than
2832
- * bright ones and shift every hue it touched.
2835
+ * Post-consolidation (2026-04): one consolidated PMTiles per region, all
2836
+ * S-57 classes as distinct MVT layers inside. The MapLibre style emits one
2837
+ * vector source (id = region) and N paint layers that reference it via
2838
+ * `source-layer` = S-57 class name.
2833
2839
  *
2834
- * The one derivation the palettes rely on, and the reason this file exists:
2835
- * NIGHT's deeper sounding SNDG1 (#897350) is SNDG2 (#c8aa78) at 43% of its
2836
- * linear light. `labels.ts` builds its NIGHT secondary text the same way, so
2837
- * the two stay one family by construction rather than by two hand-typed hexes
2838
- * that happen to agree.
2839
- */
2840
- /** WCAG 2.x relative luminance of a `#rrggbb` colour, 0 (black) to 1 (white). */
2841
- declare function relativeLuminance(hex: string): number;
2842
- /** WCAG 2.x contrast ratio between two `#rrggbb` colours, 1 to 21. */
2843
- declare function contrastRatio(a: string, b: string): number;
2844
- /**
2845
- * The same hue at `factor` times its linear light. `factor` above 1 can clip a
2846
- * channel, which shifts the hue; nothing in the palettes brightens, so callers
2847
- * pass 0–1.
2848
- */
2849
- declare function scaleLinearLight(hex: string, factor: number): string;
2850
- /**
2851
- * `hex` dimmed, hue kept, until its luminance is at most `ceiling`. A colour
2852
- * already at or under the ceiling comes back unchanged (lower-cased).
2840
+ * `buildSources()` returns either the legacy Mapbox MTS shape (preserved for
2841
+ * demos / external style export — see generateStyle comments) or the PMTiles
2842
+ * shape pointing at a single tile URL per region.
2853
2843
  */
2854
- declare function capLuminance(hex: string, ceiling: number): string;
2844
+ /** Vector source emitted for the legacy Mapbox MTS path. */
2845
+ interface MapboxVectorSource {
2846
+ type: "vector";
2847
+ url: string;
2848
+ }
2855
2849
  /**
2856
- * NIGHT's two ink levels, shared by every family's text and marks.
2857
- *
2858
- * `NIGHT_INK` S52_NIGHT.CHBLK — the palette's linework and navigation
2859
- * text. The brightest thing NIGHT draws on purpose.
2860
- * `NIGHT_MUTED_INK` that ink at 43% of its linear light, which is how
2861
- * S52_NIGHT.SNDG1 (the deeper soundings) was derived.
2862
- * Everything that orients rather than navigates is drawn
2863
- * at or under this, and `night-luminance.test.ts` holds
2864
- * every visible NIGHT colour to it unless an exemption
2865
- * names a reason.
2850
+ * Vector source emitted for the tiles.chrt.co PMTiles path. Uses an explicit
2851
+ * `tiles` array (not a TileJSON `url`) because the tile-worker serves raw MVT
2852
+ * bytes without a TileJSON descriptor.
2866
2853
  */
2867
- declare const NIGHT_INK: string;
2868
- declare const NIGHT_MUTED_INK: string;
2869
-
2854
+ interface PmtilesVectorSource {
2855
+ type: "vector";
2856
+ tiles: string[];
2857
+ minzoom: number;
2858
+ maxzoom: number;
2859
+ /** The credit a renderer shows for this source (MapLibre's attribution control). */
2860
+ attribution?: string;
2861
+ }
2870
2862
  /**
2871
- * Is this MapLibre map still there to be written to?
2872
- *
2873
- * ===========================================================================
2874
- * ⚠ ASK AFTER EVERY AWAIT. A MAP CAN BE REMOVED WHILE YOU WERE WAITING.
2875
- * ===========================================================================
2876
- *
2877
- * `map.remove()` deletes `map.style`, and after that nearly every method that
2878
- * touches the style throws rather than returning: `hasImage`, `addImage`,
2879
- * `updateImage`, `getSource`, `addLayer` all fail with "Cannot read properties
2880
- * of undefined (reading 'getImage')" and its siblings. Code that was handed a
2881
- * map, awaited a decode, an IndexedDB read or a fetch, and then wrote to the
2882
- * map, throws from wherever it resumed — usually a promise nobody is awaiting,
2883
- * so the throw is an uncaught error with no caller to explain it.
2884
- *
2885
- * Measured in CHRTdesktop (chip 63, 2026-10-04): with the page's main thread
2886
- * held at 6× CPU, the chart removed its map in the middle of a pan, and the
2887
- * S-52 image loader resumed onto it:
2888
- *
2889
- * TypeError: Cannot read properties of undefined (reading 'getImage')
2890
- * at Map.hasImage
2891
- * at addImageToMap (s52-image-cache.ts)
2892
- *
2893
- * The rule, for anything in this package or the app that takes a map: after
2894
- * an `await` (or in a `.then`, a frame callback, an `onload`), check this
2895
- * before the first call on the map, and stop quietly if it says no. A removed
2896
- * map is not an error to report — whoever removed it is building the next one.
2897
- *
2898
- * ⚠ `_removed`, NOT `style`. maplibre sets `_removed = true` as the last step
2899
- * of `remove()` (typed on `Map` in maplibre-gl 4 and 5), so it is the fact,
2900
- * where `style === undefined` is a consequence. Test doubles that never had a
2901
- * `style` read as live, which is what they are. The app pins the maplibre side
2902
- * of this (`map-live.test.ts`), so an upgrade that renames the field fails a
2903
- * test rather than silently making every check say yes.
2904
- *
2905
- * Typed `object` so the structural map shapes the app passes around (a tile
2906
- * census, an errored-tile tracker) can be asked too, not only `maplibregl.Map`.
2863
+ * Terrarium elevation, served as PNG from `CHRT_RELIEF`. Separate from the
2864
+ * vector shapes because MapLibre needs `type: "raster-dem"` and an explicit
2865
+ * `encoding` — a raster-dem source read as a plain raster renders the encoded
2866
+ * RGB as a garish false-colour image rather than as elevation.
2907
2867
  */
2908
- declare function isMapLive(map: object): boolean;
2909
-
2910
- /** S-52 display categories – controls default visibility */
2911
- type DisplayCategory = "BASE" | "STANDARD" | "OTHER" | "MARINERS" | "META";
2912
- /** Mapbox GL layer type we'll use for this entry */
2913
- type GeomType = "fill" | "line" | "circle" | "symbol";
2914
- /** Nautical groupings (loosely follows Chart No. 1 sections) */
2915
- type NauticalCategory = "Depths" | "Land" | "Navigation Aids" | "Hazards" | "Regulated Areas" | "Infrastructure" | "Meta";
2916
- /** Paint properties – a subset we actually use */
2917
- interface AtlasStyle {
2918
- type: GeomType;
2919
- paint: Record<string, unknown>;
2920
- layout?: Record<string, unknown>;
2921
- /** Nautical symbol ID for symbol-type layers */
2922
- iconImage?: string;
2923
- }
2924
- /** One row in the S-52 lookup table */
2925
- interface LookupEntry {
2926
- /** S-57 object class acronym */
2927
- objectClass: string;
2928
- /** Human-readable name */
2929
- name: string;
2930
- /** Chart No. 1 section */
2931
- category: NauticalCategory;
2932
- /** S-52 display category */
2933
- displayCategory: DisplayCategory;
2934
- /** Default style using S-52 color tokens (resolved later) */
2935
- style: AtlasStyle;
2936
- /** Whether conditional symbology may override the style */
2937
- hasConditional?: boolean;
2938
- /** Attributes that affect styling */
2939
- conditionalAttributes?: string[];
2940
- /** INT1 / Chart No. 1 reference (e.g. "Q130.1") */
2941
- int1Ref?: string;
2942
- /** S-52 viewing group number */
2943
- viewingGroup?: number;
2944
- /** Minimum compilation scale for display (SCAMIN) */
2945
- scamin?: number;
2946
- /**
2947
- * Mark this class as deliberately invisible (label-only or metadata-only)
2948
- * so the layer-coverage audit doesn't flag its geometry-type gaps as bugs.
2949
- *
2950
- * Examples:
2951
- * - SOUNDG: rendered as depth numbers via labels.ts, not as visible dots
2952
- * - LNDRGN: region NAME labels only, no shape
2953
- * - M_NPUB / M_NSYS / M_QUAL / M_SREL / DSID: S-57 metadata classes;
2954
- * present for click-query attributes, never drawn
2955
- *
2956
- * Without this flag, `scripts/audit-layer-coverage.ts` reports the
2957
- * thousands of features in these classes as "gaps" every week even
2958
- * though the invisibility is intentional.
2959
- */
2960
- auditIgnore?: boolean;
2961
- }
2962
- /** Resolved style ready for Mapbox GL (tokens → hex) */
2963
- interface ResolvedStyle {
2964
- type: GeomType;
2965
- paint: Record<string, unknown>;
2966
- layout?: Record<string, unknown>;
2967
- minzoom?: number;
2968
- maxzoom?: number;
2868
+ interface RasterDemSource {
2869
+ type: "raster-dem";
2870
+ tiles: string[];
2871
+ encoding: "terrarium";
2872
+ tileSize: number;
2873
+ minzoom: number;
2874
+ maxzoom: number;
2875
+ /** The credit a renderer shows for this source (MapLibre's attribution control). */
2876
+ attribution?: string;
2969
2877
  }
2970
-
2878
+ type VectorSourceSpec = MapboxVectorSource | PmtilesVectorSource;
2879
+ type AnySourceSpec = VectorSourceSpec | RasterDemSource;
2880
+ /** CHRTland's vector land base. ADR 0023: its own archive, never chart data. */
2881
+ declare const LAND_REGION = "CHRT_LAND";
2882
+ /** CHRTland's terrarium relief. */
2883
+ declare const RELIEF_REGION = "CHRT_RELIEF";
2884
+ /** The world basemap under everything (completing-the-world-map, 2026-08-27). */
2885
+ declare const BASE_REGION = "CHRT_BASE";
2971
2886
  /**
2972
- * S-52 lookup table covering all S-57 object classes found in NOAA ENCs.
2973
- * Colours reference S-52 tokens (resolved at render time).
2974
- * INT1 references per NOAA Chart No. 1 (Ed 13).
2887
+ * CHRT_BASE's zoom range. 11 is the deepest zoom the archive's detail tier
2888
+ * carries honestly — the planet's inland water exists from z9 and is read at
2889
+ * z10 — and the world tier is tiled to the same depth so no layer vanishes
2890
+ * from a tile the detail tier creates. Above 11 the client overzooms, and
2891
+ * overzoomed inland water looks soft DELIBERATELY: it is "roughly where the
2892
+ * lakes are", not a surveyed shoreline. Must equal the builder's MAXZOOM
2893
+ * (tools/chrtbase-build/build.mjs).
2975
2894
  */
2976
- declare const S52_LOOKUP: Record<string, LookupEntry>;
2977
-
2895
+ declare const BASE_MINZOOM = 0;
2896
+ declare const BASE_MAXZOOM = 11;
2978
2897
  /**
2979
- * S-52 draw order: layers listed bottom → top.
2980
- * Layers not in this list are inserted before point features.
2898
+ * The credit the ONLINE CHRT_BASE source carries (owner wording, 2026-09-16).
2899
+ *
2900
+ * OpenStreetMap is named because the online archive's z9–11 detail tier IS
2901
+ * OSM-derived inland water (ODbL, `public/chrtbase-derivation.json`). The
2902
+ * app-embeddable offline bundle is a z0–6 cut that contains no OSM data and
2903
+ * carries `BASE_BUNDLE_ATTRIBUTION` instead (base-layers.ts) — one credit per
2904
+ * artefact, each naming exactly the data it holds.
2981
2905
  */
2982
- declare const S52_DRAW_ORDER: string[];
2983
- /** Get numeric draw priority for a layer (lower = drawn first / below) */
2984
- declare function getDrawPriority(layerName: string): number;
2985
-
2986
- /** S-52 display modes */
2987
- type DisplayMode = "BASE" | "STANDARD" | "ALL";
2988
- /** Check if a layer's display category is visible in the given mode */
2989
- declare function isVisibleInMode(displayCategory: DisplayCategory, mode: DisplayMode): boolean;
2906
+ declare const BASE_ATTRIBUTION = "\u00A9 CHRT \u00B7 Made with Natural Earth \u00B7 \u00A9 OpenStreetMap contributors";
2990
2907
  /**
2991
- * Convert S-52 SCAMIN (scale minimum) to Mapbox GL minzoom.
2908
+ * The credit the CHRT_LAND source carries. CHRTland's land, land use, roads,
2909
+ * places, peaks and marinas are OpenStreetMap via the Protomaps planet, and the
2910
+ * ODbL requires this notice wherever the map is shown (.chrt/legal.md
2911
+ * "Required notices"). Its other inputs — the ENC's charted land, USGS 3DEP
2912
+ * contours, GNIS names — are US federal public domain and owe none. Same
2913
+ * string as the offline land bundle's (offline-worker `LAND_PROVENANCE`).
2992
2914
  */
2993
- declare function scaminToMinZoom(scamin?: number): number | undefined;
2994
- declare function getCollisionSettings(viewingGroup?: number): {
2995
- iconAllowOverlap: boolean;
2996
- iconOptional: boolean;
2997
- symbolSortKey?: number;
2998
- };
2915
+ declare const LAND_ATTRIBUTION = "\u00A9 OpenStreetMap contributors";
2999
2916
  /**
3000
- * Get the resolved Mapbox GL style for a given S-57 layer.
2917
+ * The credit the CHRT_RELIEF source carries. USGS 3DEP elevation, public
2918
+ * domain (17 U.S.C. § 105) — no notice is required, so this is a courtesy that
2919
+ * names the source. There is no OpenStreetMap in it.
3001
2920
  */
3002
- declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName, opts?: {
3003
- safetyContour?: number;
3004
- }): ResolvedStyle;
3005
- declare function getLookupEntry(layerName: string): LookupEntry;
3006
-
2921
+ declare const RELIEF_ATTRIBUTION = "Elevation: USGS 3D Elevation Program";
2922
+ /** Terrarium tiles are 512 px; the default 256 would halve the apparent relief. */
2923
+ declare const RELIEF_TILE_SIZE = 512;
3007
2924
  /**
3008
- * S-52 Conditional Symbology Procedures.
3009
- * Each function takes feature properties and returns paint overrides (hex colours).
3010
- * All expression builders accept an optional colorTable parameter.
2925
+ * How `CHRT_RELIEF`'s PNGs encode elevation. MapLibre's raster-dem default is
2926
+ * `mapbox`, which decodes terrarium RGB into nonsense heights — so every
2927
+ * surface that describes this source (this registry, the tile-worker's
2928
+ * TileJSON) has to say it.
3011
2929
  */
2930
+ declare const RELIEF_ENCODING = "terrarium";
2931
+ type SourceMode = "mapbox" | "pmtiles";
3012
2932
  /**
3013
- * Default safety contour in metres — the depth below which a feature is
3014
- * "dangerous to my hull" for portrayal purposes.
2933
+ * Query parameter carrying the resolved build id on every tile URL.
3015
2934
  *
3016
- * This is the single source of truth for the *default* used by every
3017
- * safety-contour-driven conditional symbology rule, so the rules can't
3018
- * silently disagree with each other:
3019
- *
3020
- * - §SOUNDG02 — soundings shoaler than the contour get the
3021
- * high-contrast shade (see `getLabelConfig` in labels.ts).
3022
- * - §CSYM02 — isolated dangers shoaler than the contour get the
3023
- * DNGCIR01 ring (see `buildLayersForClass` in layer-builder.ts).
3024
- * - §DEPARE04 — the MS/MD shade boundary (owner decision 2026-08-21,
3025
- * replacing a hardcoded 5 m middle cut; see the DEPARE block below
3026
- * for the full IHO correspondence).
2935
+ * ── Why the build has to be IN THE URL ────────────────────────────────────
3027
2936
  *
3028
- * All are overridden by the user's setting (sounding panel slider,
3029
- * `safetyContour` in the style options); this is only the fallback when
3030
- * none is supplied. It matches `custom-style.ts`'s config default.
3031
- */
3032
- declare const DEFAULT_SAFETY_CONTOUR_M = 10;
3033
- declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
3034
- /**
3035
- * The colour token of each `case` branch of `depareFillExpression`, in branch
3036
- * order, then the fallback. `depth-area-opaque.ts` names the layers it splits
3037
- * the expression into from this list, so the two read from one place.
3038
- */
3039
- declare const DEPARE_FILL_BRANCH_TOKENS: readonly ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
3040
- declare const DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
3041
- declare function depareFillExpression(ct?: ColorTableName, safetyContourM?: number): unknown[];
3042
- declare function lightColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3043
- declare function lightFillExpression(ct?: ColorTableName): unknown[];
3044
- declare function lateralColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3045
- declare function lateralFillExpression(ct?: ColorTableName): unknown[];
3046
- declare function hazardColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3047
- declare function hazardFillExpression(ct?: ColorTableName): unknown[];
3048
- declare function daymarkColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3049
-
3050
- /**
3051
- * Label configurations for S-57 layers.
3052
- * Defines which layers get text labels and how to extract label text.
3053
- */
3054
-
3055
- /**
3056
- * Feature-class hierarchy for SEAARE labels. Returns a tier number 1-5 per
3057
- * feature:
2937
+ * The Worker reports the build it served in the `X-Build-Id` RESPONSE HEADER
2938
+ * and sends `Cache-Control: private, max-age=3600`. A header is not part of a
2939
+ * cache key, so every build shares one URL: after a cutover, tiles the client
2940
+ * already fetched keep coming from its own HTTP cache — the old build — while
2941
+ * newly-panned-to tiles come from the new one, for up to the full hour. The
2942
+ * client renders TWO BUILDS AT ONCE.
3058
2943
  *
3059
- * Exported (with the other tier builders below) for the build-side parity
3060
- * test: CHRTbaremetal replicates these taxonomies as data in
3061
- * tools/chrt-baremetal/server/src/tilebuild/placeNameTiers.ts to stamp
3062
- * `tier` onto PLACENAME index features, and
3063
- * placename-tier-parity.test.ts evaluates the two against shared
3064
- * fixtures. Change a branch here and that test names the drift.
2944
+ * On a nautical chart that is not a cosmetic seam. Measured live 2026-08-25,
2945
+ * cutting `CHRT_LAND` from `463b265e` to `dbb69074` (a build that deliberately
2946
+ * changes land extent): adjacent tiles disagreed about whether land was there,
2947
+ * at the seam between them. Putting the build in the key gives each build its
2948
+ * own cache namespace — old entries age out untouched, and the next style
2949
+ * build flips the whole viewport at once instead of tile by tile.
3065
2950
  *
3066
- * 1 — Ocean (OBJNAM contains "<X> Ocean" — CATSEA has no
3067
- * ocean code; the attribute is 1/unset on oceans)
3068
- * 2 — Sea / Gulf (OBJNAM "Gulf of …" / "… Sea")
3069
- * 3 — Bay / Sound / (CATSEA in {5 bay, 12 narrows} OR OBJNAM
3070
- * Strait / Narrows " Bay" / " Sound" / " Strait")
3071
- * 4 — Anchorage detail (OBJNAM contains "Anchorage Area")
3072
- * 5 — Relief + default (CATSEA relief codes — authoritative — or
3073
- * relief-term names, or anything uncategorised;
3074
- * suppressed below z9 by the size curve)
2951
+ * ── Why a QUERY PARAMETER and not a path segment ──────────────────────────
3075
2952
  *
3076
- * Order matters: tier-4/5 demotions (anchorages, relief) run BEFORE the
3077
- * promotion shims so "Bear Seamount" (contains " Sea") and "…Anchorage
3078
- * Area" compound names can't bubble up into headline tiers.
3079
- * `coalesce(CATSEA, catsea)` handles post-tippecanoe case variation
3080
- * (matches `s52-conditional.ts` precedent).
3081
- */
3082
- declare function buildSeaAreaTierExpression(): ExpressionSpecification;
3083
- /**
3084
- * Built-up area tier (BUAARE): major city / town / neighborhood.
3085
- * 1 — Major city (CATBUA=1 urban, or OBJNAM length <= 8)
3086
- * 2 — Town (CATBUA=5 village, or OBJNAM length <= 14)
3087
- * 3 — Neighborhood (longer compound names)
2953
+ * A path segment would be the tidier-looking URL and it is the wrong trade
2954
+ * here, for two reasons that are both about other people's code:
3088
2955
  *
3089
- * Tier 1/2 may fire via CATBUA when populated; OBJNAM length is the
3090
- * fallback so unpopulated CATBUA still produces a usable hierarchy.
3091
- */
3092
- declare function buildBuiltUpAreaTierExpression(): ExpressionSpecification;
3093
- /**
3094
- * Land region tier (LNDRGN): major region vs. local point.
3095
- * 1 — Major region (OBJNAM length <= 12 AND no comma — "Long Island", "Cape Cod")
3096
- * 2 — Local point (else — "Bird Island", "Cottage Park")
2956
+ * 1. COMPATIBILITY IS PERMANENT, NOT "one release". iOS and desktop are
2957
+ * prod-only clients that ship on their own cadence. The Worker routes on
2958
+ * `url.pathname` (`TILE_ROUTE_PATH`) and reads only `?build=`, so a URL
2959
+ * carrying this parameter routes BYTE-IDENTICALLY to one without it. A
2960
+ * new path segment would instead need a second route regex kept alive
2961
+ * indefinitely.
2962
+ * 2. FIVE SEPARATE PATTERNS ALREADY MATCH THIS URL, and every one of them
2963
+ * ends `(?:\?.*)?$` — `src/lib/map/tile-token-client.ts` TILE_URL_PATH
2964
+ * and `src/site/chart/sail-tile-token.ts` (which decide whether a tile
2965
+ * gets its Bearer), `packages/sdk/src/tile-auth.ts` TILE_URL_PATH,
2966
+ * `public/chrt-tile-sw.js` TILE_URL_RE (which also reads the region out of
2967
+ * the URL to pick its per-region cache), and
2968
+ * `src/site/offline/pack-protocol.ts` TILE_URL (which rewrites a tile to
2969
+ * a downloaded pack). Their optional PATH segment, by contrast, is
2970
+ * `[a-z][a-z0-9-]*` — letter-first so it cannot be confused with the
2971
+ * numeric `{z}` — and build ids are hex (`463b265e`, `4de67273`). A build
2972
+ * in the path would miss every one of them, and a tile those patterns
2973
+ * miss does not merely go uncached: it goes out with NO BEARER and comes
2974
+ * back 401, which MapLibre records as a permanently errored tile. That is
2975
+ * the exact failure the `.png` drift caused on staging-sail (see
2976
+ * `chrt-tile-sw.js`).
3097
2977
  *
3098
- * No CATLND analog for region scope, so OBJNAM is the only signal.
3099
- * The comma-absence test is a heuristic: NOAA compound names like
3100
- * "Boston Harbor, MA Castle Island" carry a comma and belong at deeper
3101
- * zoom regardless of length.
3102
- */
3103
- declare function buildLandRegionTierExpression(): ExpressionSpecification;
3104
- /**
3105
- * Land area tier (LNDARE): major vs. local.
3106
- * 1 — Major (OBJNAM length <= 12)
3107
- * 2 — Local
2978
+ * The query string is also what the caches this fix targets actually key on:
2979
+ * the browser HTTP cache, MapLibre's own tile cache, and the Service Worker's
2980
+ * `new Request(req.url)` entry key all include it.
3108
2981
  *
3109
- * No category attribute. OBJNAM length is the only signal.
3110
- */
3111
- declare function buildLandAreaTierExpression(): ExpressionSpecification;
3112
- /**
3113
- * Lake tier (LAKARE): lake vs. reservoir/pond.
3114
- * 1 — Lake (CATLAK=1, or OBJNAM length <= 12)
3115
- * 2 — Reservoir/pond (CATLAK in {2,3}, or longer name)
3116
- */
3117
- declare function buildLakeTierExpression(): ExpressionSpecification;
3118
- /**
3119
- * River tier (RIVERS): major vs. local.
3120
- * 1 — Major river (OBJNAM length <= 15 AND no comma)
3121
- * 2 — Local creek (else)
3122
- */
3123
- declare function buildRiverTierExpression(): ExpressionSpecification;
3124
- /**
3125
- * Airport tier (AIRARE): major airport vs. small airstrip.
3126
- * 1 — Major (OBJNAM length <= 25 — e.g. "Logan International Airport")
3127
- * 2 — Airstrip
2982
+ * `v` rather than `build`: `?build=` is a taken name with a strict meaning on
2983
+ * the Worker (`?build=candidate`, employee-only, 400 on any other value).
3128
2984
  */
3129
- declare function buildAirportTierExpression(): ExpressionSpecification;
3130
- interface LabelLayerConfig {
3131
- /** Mapbox source layer type */
3132
- type: "symbol";
3133
- /** Text field expression or string */
3134
- textField: ExpressionSpecification | string;
3135
- /** Text size in pixels */
3136
- textSize: number | ExpressionSpecification;
3137
- /** Text color (hex) or data-driven expression */
3138
- textColor: string | ExpressionSpecification;
3139
- /** Halo color for contrast */
3140
- textHaloColor: string;
3141
- /** Halo width — pixels or zoom-interpolation expression */
3142
- textHaloWidth: number | ExpressionSpecification;
3143
- /** Text offset [x, y] in ems */
3144
- textOffset: [number, number];
3145
- /** Font stack */
3146
- textFont?: string[];
3147
- /** Additional layout properties */
3148
- layout?: Record<string, unknown>;
3149
- /** Minimum zoom level for this label */
3150
- minzoom?: number;
3151
- }
2985
+ declare const BUILD_PARAM = "v";
3152
2986
  /**
3153
- * Optional palette-and-data-driven options for label config.
2987
+ * One tile-URL template — the single place a CHRT tile URL is spelled.
3154
2988
  *
3155
- * Currently used by SOUNDG to pick palette-aware text/halo colors and to
3156
- * drive the S-52 safety-contour two-shade rule. Other classes ignore
3157
- * these options.
2989
+ * `buildId` is encoded rather than interpolated raw: MapLibre substitutes
2990
+ * `{z}`/`{x}`/`{y}` by brace-matching the template, so an id containing a
2991
+ * brace would corrupt every tile request. Empty / whitespace-only ids emit no
2992
+ * parameter at all instead of a bare `?v=` that would key its own useless
2993
+ * cache namespace.
3158
2994
  */
3159
- interface LabelConfigOptions {
3160
- /** S-52 colour table — drives palette-aware text and halo colors. */
3161
- colorTable?: ColorTableName;
2995
+ declare function tileTemplate(base: string, region: string, extension: "mvt" | "png", buildId?: string): string;
2996
+ /** Canonical source id used in `generateStyle()` — matches the region name
2997
+ * of the one consolidated PMTiles served by the tile-worker for this region. */
2998
+ declare const DEFAULT_REGION = "NOAA_ENC";
2999
+ interface BuildSourcesOptions {
3000
+ /** Which tile path to emit. Default: "pmtiles" (the supported path post-Mapbox). */
3001
+ mode?: SourceMode;
3002
+ /** Chart region. Default: "NOAA_ENC". */
3003
+ region?: string;
3162
3004
  /**
3163
- * Safety contour depth in metres. Soundings shoaler than this render in
3164
- * `SNDG2` (high-contrast); deeper soundings render in `SNDG1` (muted).
3165
- * S-52 §SOUNDG02 conditional symbology.
3005
+ * Build id of the CHART region (`region`) — the `build_tag` a
3006
+ * `mint-tile-token` response hands back.
3007
+ *
3008
+ * Emitted as the {@link BUILD_PARAM} query parameter on that region's tile
3009
+ * template. See {@link tileTemplate} for why a build discriminator has to be
3010
+ * in the URL at all.
3011
+ *
3012
+ * ⚠ THIS COVERS THE CHART REGION ONLY. A token is minted for one chart
3013
+ * region and `CHRT_LAND` / `CHRT_RELIEF` ride it, so the JWT's `build_tag`
3014
+ * says nothing about a land cutover — the same blind spot that made the
3015
+ * Worker's edge-cache key wrong until `TILE_CACHE_VERSION` v3
3016
+ * (cloudflare/tile-worker/src/cache-encoding.ts). Pass {@link buildTags} to
3017
+ * discriminate the basemap sources too.
3166
3018
  */
3167
- safetyContour?: number;
3019
+ buildTag?: string;
3168
3020
  /**
3169
- * When false, all soundings render in `SNDG2` (single-shade). Default
3170
- * true — emit the safety-contour case expression.
3021
+ * Resolved build id per region — `{ NOAA_ENC: "…", CHRT_LAND: "…" }`.
3022
+ *
3023
+ * The complete form of {@link buildTag}: every source this style declares
3024
+ * gets its own discriminator, so a `CHRT_LAND` promotion turns over the land
3025
+ * URLs without touching the chart's (and vice versa). An entry here wins
3026
+ * over `buildTag` for the same region.
3027
+ *
3028
+ * This is the mint response's `build_tags` map verbatim (claim contract
3029
+ * v1.7) — `TileTokenResponse.buildTags` in both `lib/map/tile-token-client.ts`
3030
+ * and the SDK's `tile-auth.ts`. The same map already keys the tile Service
3031
+ * Worker's per-region caches; passing it here puts the same identity in the
3032
+ * URL, so the layer above the Service Worker stops mixing builds too.
3033
+ *
3034
+ * A region absent from the map simply gets no discriminator — the URL it
3035
+ * has always had. That is a deliberate soft edge: an older mint response, or
3036
+ * a region with no servable build, must degrade to today's behaviour rather
3037
+ * than emit a URL pinned to a build nobody resolved.
3171
3038
  */
3172
- twoShadeSoundings?: boolean;
3039
+ buildTags?: Readonly<Record<string, string>>;
3040
+ /** Override tile-worker origin (e.g. staging). Default: DEFAULT_TILE_BASE_URL. */
3041
+ tileBaseUrl?: string;
3173
3042
  /**
3174
- * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
3175
- * numerals at small sizes. Default "semibold".
3043
+ * Emit the CHRTland vector land base as a second source (ADR 0023). Off by
3044
+ * default: it costs a source, and the region must be granted on the token.
3176
3045
  */
3177
- soundingFontWeight?: "regular" | "semibold";
3046
+ land?: boolean;
3047
+ /**
3048
+ * Emit CHRTland's terrarium relief as a raster-dem source. Separate from
3049
+ * `land` because relief needs a raster-capable worker AND a grant that
3050
+ * `land-regions.ts` still withholds.
3051
+ */
3052
+ relief?: boolean;
3053
+ /**
3054
+ * Emit the CHRT_BASE world basemap as a third vector source — the dark
3055
+ * geography under everything, the tan jurisdiction fill, and the rough
3056
+ * inland water inside countries CHRT carries. Its layers sit UNDER the
3057
+ * no-coverage hatch's companions and everything else; see baseLayers().
3058
+ */
3059
+ base?: boolean;
3178
3060
  }
3179
3061
  /**
3180
- * Two-tier label rendering strategy for the band-priority style.
3062
+ * Build the "sources" block for a CHRT style.
3181
3063
  *
3182
- * `feature-band-gated` — emit one variant per source band, each gated to
3183
- * its band's zoom window (the existing `makeBandVariants` behaviour). Right
3184
- * for dense feature labels where seeing the same buoy designation or
3185
- * sounding twice across overlapping bands is visual noise.
3064
+ * - `mode: "mapbox"`: one mapbox:// url (kept for external style export only).
3065
+ * - `mode: "pmtiles"` (default): one vector tile endpoint for the region,
3066
+ * routed by the Worker to the one consolidated PMTiles:
3067
+ * `${tileBaseUrl}/v1/tiles/{region}/{z}/{x}/{y}.mvt`
3186
3068
  *
3187
- * `orientation-cross-band` — emit a SINGLE label layer with no band
3188
- * filter. The per-class `minzoom` is honoured. Right for orientation
3189
- * "where am I" labels (sea-area names, land regions, cities) — these
3190
- * must persist as the user zooms in, even if the broader-scope cell
3191
- * carrying the OBJNAM is from a coarser band than the one rendering
3192
- * features at the current zoom. Collision arbitration via
3193
- * `ORIENTATION_BAND_SORT_KEY` gives the broader-scope (lower-band) copy
3194
- * priority where the same OBJNAM exists in multiple bands.
3069
+ * Pass `buildTag` / `buildTags` to pin each source's URL to the build it is
3070
+ * serving — without that, one URL is shared by every build and a cutover
3071
+ * leaves the client rendering two of them at once. See {@link BUILD_PARAM}.
3195
3072
  */
3196
- type LabelBehavior = "feature-band-gated" | "orientation-cross-band";
3197
- declare const LABEL_BEHAVIOR: Record<string, LabelBehavior>;
3198
- /** Returns the rendering strategy for an S-57 class. Unknown classes default to band-gated. */
3199
- declare function getLabelBehavior(layerName: string): LabelBehavior;
3073
+ declare function buildSources(opts?: BuildSourcesOptions): Record<string, AnySourceSpec>;
3074
+
3200
3075
  /**
3201
- * Class-based label priority ladder — THE single collision-priority model
3202
- * for feature labels (portrayal-excellence P4). Converges the previous
3203
- * per-mechanism patches (SAFETY_CRITICAL_LABELS allow-overlap, the
3204
- * band-5/6-only Harbor collision discipline) into one explicit ranking.
3076
+ * DEPTH NUMBERS — how big the chart draws its depth figures.
3205
3077
  *
3206
- * Rank semantics: LOWER rank = higher priority. The rank drives two
3207
- * mechanisms that must stay in agreement:
3078
+ * Asked for from the helm (chip 68, 2026-10-05), by a captain running CHRTsail
3079
+ * desktop on a Raspberry Pi: *"I still need the depth numbers to be bolder
3080
+ * which may be a setting. I just can't sail the boat and adjust all that stuff
3081
+ * at the same time."* A set-and-forget choice, then, not a slider.
3208
3082
  *
3209
- * 1. `symbol-sort-key` — every feature label carries
3210
- * `rank*1000 + within-class-key`, so if label layers are ever merged
3211
- * (the P8 style fan-out reduction), placement priority survives the
3212
- * merge unchanged.
3213
- * 2. Label LAYER ORDER — MapLibre places symbols topmost-layer-first, so
3214
- * `style-generator.ts` emits feature-label layers lowest-priority
3215
- * first: safety-critical classes end up topmost and claim collision
3216
- * space before anything else. (`symbol-sort-key` alone cannot do
3217
- * this — it only orders placement WITHIN a layer.)
3083
+ * ===========================================================================
3084
+ * WHAT "LARGE" CHANGES, AND WHAT IT IS NOT ALLOWED TO TOUCH
3085
+ * ===========================================================================
3218
3086
  *
3219
- * The ladder (safety-critical > nav-aids > depth field > structures >
3220
- * ambient notes):
3087
+ * It changes the SIZE of the glyphs (and, where a class was Regular, the
3088
+ * WEIGHT, to the Semibold soundings already use), the halo a little — see
3089
+ * `LARGE` for why less than the glyph — and the sounding collision padding —
3090
+ * see `LARGE_SOUNDING_PADDING_PX` for why that one moves.
3221
3091
  *
3222
- * 100 — Safety-critical: light characteristics (LIGHTS), hazard depth
3223
- * callouts (UWTROC/OBSTRN/WRECKS VALSOU), overhead clearances
3224
- * (CBLOHD/PIPOHD VERCLR). Irreducible mariner-safety text.
3225
- * 200 — Nav-aid designations: buoy / beacon `R "4"` labels.
3226
- * 250 — Auxiliary nav-aid info: fog signal type (FOGSIG).
3227
- * 300 — Depth field: soundings, contour values. Individually droppable —
3228
- * a hidden sounding has visible neighbors.
3229
- * 400 — Structures: bridges, landmarks, piers, moorings, buildings.
3230
- * 500 — Ambient annotation: seabed nature, magnetic variation, kelp.
3231
- * 900 — Orientation names (SEAARE etc.) — not part of this ladder;
3232
- * they keep their settled watermark model (cell-band fan +
3233
- * dominance composition + `orientationLabelLayout`) and always
3234
- * paint underneath feature labels.
3092
+ * It does not touch, and `depth-label-size.test.ts` holds every one of these
3093
+ * equal to the Standard style in every palette and every symbol family:
3235
3094
  *
3236
- * Safety guardrail: a lower rung never blocks a higher rung — placement
3237
- * order guarantees the safety-critical label got the space first. Density
3238
- * relief comes from dropping the LOWEST rung in a pile-up, never the top.
3239
- */
3240
- declare const LABEL_PRIORITY: Record<string, number>;
3241
- /**
3242
- * Resolve the priority rank for an S-57 class's label. Orientation classes
3243
- * report 900 regardless of ladder membership (PRDARE etc.) so the
3244
- * layer-order sort in style-generator.ts can treat priority uniformly.
3245
- */
3246
- declare function getLabelPriority(layerName: string): number;
3247
- /**
3248
- * Compose a `symbol-sort-key` from a class rank and an optional
3249
- * within-class key expression. The within-class key is clamped to
3250
- * [0, 999] so it can never bleed into a neighboring rank's band.
3251
- */
3252
- declare function labelSortKey(rank: number, within?: unknown): ExpressionSpecification | number;
3253
- /**
3254
- * Extract the trailing designation token from a nav-aid OBJNAM.
3095
+ * - `text-field` — the number itself, its unit conversion, its rounding.
3096
+ * ENC content is never altered; a bigger "4" is still the cell's "4".
3097
+ * - `text-color` / `text-halo-color` — §SOUNDG02's two shades against the
3098
+ * safety contour, and the palette that draws them (red night included:
3099
+ * it IS the NIGHT chart). A depth number shoaler than the contour stays
3100
+ * the loud one.
3101
+ * - `symbol-sort-key` — shallowest wins a collision, at any size.
3102
+ * - `filter`, `minzoom`, `maxzoom`, layer ids and layer order — so the
3103
+ * resident layer set the desktop builds at boot is the same set, and a
3104
+ * switch is `setLayoutProperty` / `setPaintProperty` on layers already
3105
+ * there. No layer and no draw call is added; what costs more is GPU fill,
3106
+ * because a bigger glyph covers more pixels (about 6–7 % more blended
3107
+ * fragments per pixel on the chart bench — the PR has the numbers).
3255
3108
  *
3256
- * NOAA names channel aids "<waterway> <type> Buoy|Beacon <designation>"
3257
- * where the designation is 1-4 alphanumerics: "4", "18A", "B", "BG".
3258
- * Returns null when the name has no designation (trailing token is a
3259
- * stopword type-name, or longer than 4 chars).
3109
+ * ===========================================================================
3110
+ * WHICH LABELS ARE "DEPTH NUMBERS"
3111
+ * ===========================================================================
3260
3112
  *
3261
- * This is the TS twin of `navAidDesignationTextExpression` — the GeoJSON
3262
- * preprocessing path uses this function, the PMTiles path uses the GL
3263
- * expression. Keep the two in agreement (see label-priority.test.ts).
3113
+ * Exactly the ones the depth-unit switch converts (`buildLabelLayer` in
3114
+ * `layer-builder.ts`): soundings, contour values, and the depth printed beside
3115
+ * a rock, wreck or obstruction. The unit switch already treats those three as
3116
+ * one claim — its own note says a rock's figure "is the SAME depth claim as a
3117
+ * sounding" — and a size switch that enlarged the soundings around a rock but
3118
+ * left the rock's own depth small would make the hazard the quietest number
3119
+ * on that part of the chart.
3120
+ *
3121
+ * Not the dredged-channel note (`DRGNOTE`): it is a sentence fitted inside
3122
+ * its own channel polygon at a computed size, and scaling it would stop it
3123
+ * fitting. Not the bridge note: a clearance is a height, not a depth.
3124
+ *
3125
+ * ===========================================================================
3126
+ * WHY NOT A BOLD FACE
3127
+ * ===========================================================================
3128
+ *
3129
+ * The chart's glyph sets are Open Sans Regular, Semibold and Italic — the tile
3130
+ * worker's `/v1/glyphs` (`cloudflare/tile-worker/glyphs/GLYPHS.json`) and the
3131
+ * offline chart-assets pack serve those three and nothing else, and a
3132
+ * fontstack no glyph host serves is the failure `labels.ts`'s SOUNDG note
3133
+ * describes (Native lost every sounding). Soundings are already Semibold. So "bolder" here is
3134
+ * size: stroke width scales with the glyph, and at 1x device pixels (a Pi's
3135
+ * panel) a 14 px Semibold numeral is both heavier and crisper than a 10 px one.
3264
3136
  */
3265
- declare function extractNavAidDesignation(objnam: string): string | null;
3266
- /** Get label config for a given S-57 layer, or null if no labels */
3267
- declare function getLabelConfig(layerName: string, opts?: LabelConfigOptions): LabelLayerConfig | null;
3137
+
3138
+ /** The two choices, in presentation order. */
3139
+ declare const DEPTH_LABEL_SIZES: readonly ["standard", "large"];
3140
+ type DepthLabelSize = (typeof DEPTH_LABEL_SIZES)[number];
3268
3141
  /**
3269
- * Build a Mapbox symbol layer spec from a label config.
3142
+ * Standard is the chart as it has always been drawn, and it is what a style
3143
+ * generated without the option gets: byte-identical, so the served style, the
3144
+ * native-render goldens and every client that never asks are unchanged.
3270
3145
  */
3271
- declare function buildLabelLayer(layerId: string, sourceId: string, config: LabelLayerConfig, visible: boolean, overrides?: {
3272
- textScale?: number;
3273
- minzoom?: number | null;
3274
- allowOverlap?: boolean;
3275
- }): maplibregl.LayerSpecification;
3146
+ declare const DEFAULT_DEPTH_LABEL_SIZE: DepthLabelSize;
3147
+ /** Case-insensitive; anything unrecognised is `null`, never a throw. */
3148
+ declare function parseDepthLabelSize(raw: string | null | undefined): DepthLabelSize | null;
3149
+ /** The S-57 classes whose label is a depth figure. See the header. */
3150
+ declare const DEPTH_LABEL_CLASSES: ReadonlySet<string>;
3276
3151
  /**
3277
- * Pre-process SOUNDG GeoJSON to extract Z coordinates as `depth` properties.
3278
- * Soundings are MultiPoint with Z values in geometry.coordinates[][2].
3279
- * This explodes MultiPoints into individual Points with a `depth` property.
3152
+ * Sounding collision padding at Large, px. Standard is 6.
3153
+ *
3154
+ * ⚠ THIS IS THE ONE THAT DECIDES HOW MANY SOUNDINGS SURVIVE. MapLibre drops a
3155
+ * sounding whose box (glyphs + padding) overlaps one already placed — the
3156
+ * shallowest places first, so what drops is the deeper figure. A 1.4x numeral
3157
+ * inside Standard's 6 px padding lost a quarter of the soundings: bigger
3158
+ * numbers, fewer of them, which is not what was asked for. Trimming the
3159
+ * padding gives the box back.
3160
+ *
3161
+ * Measured 2026-10-05 on the chart bench (`scripts/chart-bench`, Boston, DAY,
3162
+ * 1280x800 at 1x), soundings placed, Standard → Large at this padding:
3163
+ *
3164
+ * z11.5 515 → 539 (+5%) z12.3 434 → 415 (−4%)
3165
+ * z13 374 → 366 (−2%) z14 326 → 311 (−5%)
3166
+ *
3167
+ * and at Standard's 6 px: 391 / 246 at z11.5 / z14 (−24% / −25%). 3 px kept
3168
+ * z14 level but put 17% MORE figures on the coastal view, which reads as
3169
+ * clutter rather than as the same chart drawn bigger.
3280
3170
  */
3281
- declare function preprocessSoundings(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3171
+ declare const LARGE_SOUNDING_PADDING_PX = 3.5;
3282
3172
  /**
3283
- * Pre-process LIGHTS GeoJSON to derive compact light characteristic labels.
3284
- * Adds `_lightLabel` property (e.g., "Fl(2) WR 4s 5M").
3173
+ * Apply a depth-label size to one generated label layer.
3285
3174
  *
3286
- * KEEP IN STEP WITH `buildLightLabelExpression()` in layer-builder.ts — that
3287
- * is the same label built as a GL expression for the tile path, which has no
3288
- * JS preprocessing step. `light-label-parity.test.ts` asserts the two agree
3289
- * on real tile features; if you change one, change both.
3175
+ * Standard — and any class that is not a depth figure — returns the SAME
3176
+ * object, untouched. Large returns a copy with only the properties listed in
3177
+ * the header changed.
3290
3178
  */
3291
- declare function preprocessLights(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3292
- /** List of layers that have labels */
3293
- declare const LABELED_LAYERS: Set<string>;
3179
+ declare function applyDepthLabelSize(objectClass: string, layer: StyleLayer, size: DepthLabelSize | undefined): StyleLayer;
3180
+
3294
3181
  /**
3295
- * Pre-process buoy/beacon GeoJSON to derive concise mariner-style labels.
3296
- * Adds `_navAidLabel` property (e.g., 'R "8"', 'G "5"').
3182
+ * Builds Mapbox GL Style layer specs from S-52 lookup entries.
3183
+ * Wraps the existing atlas resolveStyle() and adds source/source-layer/filter.
3297
3184
  */
3298
- declare function preprocessNavAids(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3299
- /** Check if a layer name needs nav aid preprocessing */
3300
- declare function isNavAidLayer(layerName: string): boolean;
3185
+
3186
+ interface StyleLayer {
3187
+ id: string;
3188
+ type: string;
3189
+ source: string;
3190
+ "source-layer": string;
3191
+ filter?: unknown[];
3192
+ paint: Record<string, unknown>;
3193
+ layout?: Record<string, unknown>;
3194
+ minzoom?: number;
3195
+ maxzoom?: number;
3196
+ metadata?: Record<string, unknown>;
3197
+ }
3301
3198
  /**
3302
- * Pre-process water body polygon GeoJSON into centroid points for label placement.
3303
- * Computes a simple average-coordinate centroid for each polygon feature that has OBJNAM.
3199
+ * Build all Mapbox GL layers for a given S-57 object class.
3200
+ * Returns multiple layers (fill, outline, symbol, label, etc.).
3201
+ *
3202
+ * Post-consolidation: all S-57 classes live as MVT layers inside one
3203
+ * consolidated PMTiles per region; `source` is always the region name.
3204
+ *
3205
+ * `opts.safetyContour` is the mariner's safety depth in metres — it drives
3206
+ * the §CSYM02 isolated-danger ring on hazard classes. Defaults to
3207
+ * `DEFAULT_SAFETY_CONTOUR_M`, the same default the SOUNDG two-shade rule
3208
+ * uses, so the ring and the sounding colour always agree.
3304
3209
  */
3305
- declare function preprocessWaterBodies(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3306
- /** MVT layer the build-side place-name index emits. */
3307
- declare const PLACENAME_SOURCE_LAYER = "PLACENAME";
3308
- /** Orientation classes whose names render italic (water voice). */
3309
- declare const PLACENAME_WATER_CLASSES: readonly string[];
3310
- /** Orientation classes whose names render upright (land voice). */
3311
- declare const PLACENAME_LAND_CLASSES: readonly string[];
3210
+ declare function buildLayersForClass(objectClass: string, ct: ColorTableName, sourceId?: string, opts?: {
3211
+ safetyContour?: number;
3212
+ }): StyleLayer[];
3312
3213
  /**
3313
- * The 2 PLACENAME symbol layers. Layout contract (design §2.2): collision
3314
- * ON (`allow-overlap: false`, `ignore-placement: false`,
3315
- * `optional: true`), padding 4, `text-variable-anchor`, and
3316
- * `symbol-sort-key` straight from the build-side `rank` — MapLibre places
3317
- * LOWER sort keys first, and rank is minted lower-is-more-important
3318
- * (tier major, bbox area minor), so when space runs out the biggest names
3319
- * survive. That ranking is the per-zoom density governor P4 asked for.
3214
+ * Get the full ordered list of object classes for style generation.
3215
+ * Uses S52_DRAW_ORDER as base, then adds any missing classes from the
3216
+ * S-52 lookup table so a newly-ingested class automatically gets styled
3217
+ * without touching this file.
3320
3218
  */
3321
- declare function buildPlaceNameLayers(sourceId: string, ct?: ColorTableName): maplibregl.LayerSpecification[];
3219
+ declare function getOrderedClasses(): string[];
3322
3220
 
3323
3221
  /**
3324
- * Spatial grid-based feature decimation for performance at low zoom levels.
3325
- * Thins dense GeoJSON feature sets by keeping only one representative
3326
- * feature per grid cell per layer.
3327
- */
3328
- interface DecimationOptions {
3329
- /** Grid resolution (cells per axis). Default 80. */
3330
- gridSize?: number;
3331
- /** Zoom level below which decimation kicks in. Default 12. */
3332
- zoomThreshold?: number;
3333
- }
3334
- /**
3335
- * Zoom-adaptive grid resolution for decimation.
3336
- */
3337
- declare function getGridSizeForZoom(zoom: number): number;
3338
- /**
3339
- * Decimate a GeoJSON FeatureCollection using spatial grid thinning.
3340
- * Only runs when zoom < zoomThreshold. Returns the original FC otherwise.
3222
+ * Dominance fan-out collapse (P8 Phase 3.3 / D4 "axis 1").
3341
3223
  *
3342
- * @param fc - Input FeatureCollection
3343
- * @param layerName - S-57 layer name (affects priority logic)
3344
- * @param currentZoom - Current map zoom level
3345
- * @param bounds - Current viewport bounds
3346
- * @param opts - Decimation options
3224
+ * `makeBandVariants()` historically emitted, per (class, band): one
3225
+ * "not-dominated" layer plus up to five `-domBy{D}` copies that differ ONLY
3226
+ * in their dominance filter and a `maxzoom` of `dominatedVariantMaxzoom(D)`.
3227
+ * That fan existed because MapLibre filters could not read `["zoom"]`. They
3228
+ * can now (style-spec 24.8.5; buckets evaluate at the tile's `overscaledZ`),
3229
+ * so the whole fan folds into ONE layer per (class, band) whose filter
3230
+ * carries the zoom gate.
3231
+ *
3232
+ * **Construction principle — read before changing anything here.** The
3233
+ * collapsed filter is the literal `["any"]` UNION of the clauses the fan
3234
+ * already encoded: every variant the legacy emitter would have produced
3235
+ * contributes exactly one `["all", <its dominance filter>, ["<", ["zoom"],
3236
+ * <its maxzoom>]]` clause, and the dead-variant skip is mirrored too.
3237
+ * Equivalence is therefore *structural*, not re-derived — which matters,
3238
+ * because the two "obvious" hand-written forms are both wrong:
3239
+ *
3240
+ * 1. A `["match", ["get","_dominated_by_min"], 2,5, 3,7, 4,9, 5,11, 6,11, 24]`
3241
+ * threshold (the form the design doc proposed) uses band 6's *eligibility*
3242
+ * zoom 11. The real ceiling is `dominatedVariantMaxzoom("6") = 13` — band
3243
+ * 6's DATA floor. Using 11 reopens the z11–z12 void window that
3244
+ * `band-priority.ts` documents closing.
3245
+ * 2. That same form silently drops the legacy `_dominated: true` boolean,
3246
+ * which `dominatedByMinFilter("6")` still honours. A legacy-tagged feature
3247
+ * has no `_dominated_by_min`, so `match` falls to its default and the
3248
+ * feature renders FOREVER instead of dropping at z13.
3249
+ *
3250
+ * Deriving the gate from the emitter's own clauses avoids both by construction.
3251
+ * `band-collapse.test.ts` re-proves it on every CI run by differentially
3252
+ * evaluating legacy-vs-collapsed through MapLibre's real three gates.
3253
+ *
3254
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
3255
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
3256
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
3257
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
3258
+ * for feature layers — it is unsound for the opaque-fill classes
3259
+ * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
3260
+ * order is the ONLY thing separating the bands, and 15/15 class pairs are
3261
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
3262
+ * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
3263
+ *
3264
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
3265
+ * collapses its whole render pass above 482 symbol layers, and the served
3266
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
3267
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
3268
+ * orientation fan's per-dominator variants into its per-cell layers. That is
3269
+ * NOT the killed axis 2, because what the fill argument protects — paint
3270
+ * order as the sole band separator — does not apply to labels: their
3271
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
3272
+ * exactly one layer in its ladder slot), and within-class placement priority
3273
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
3274
+ * documents as designed to survive precisely this merge.
3275
+ *
3276
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
3277
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
3278
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
3279
+ * data floor, on the live public Style API path. Renaming the id silently
3280
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
3281
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
3282
+ * clauses a customer override cannot touch), which supersedes the clamp —
3283
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
3284
+ * Both halves asserted in `band-collapse.test.ts`.
3347
3285
  */
3348
- declare function decimateFeatures(fc: GeoJSON.FeatureCollection, layerName: string, currentZoom: number, bounds: {
3349
- west: number;
3350
- south: number;
3351
- east: number;
3352
- north: number;
3353
- }, opts?: DecimationOptions): GeoJSON.FeatureCollection;
3286
+
3354
3287
  /**
3355
- * Get the max number of cells to load based on zoom level.
3288
+ * Which emitter the style generator uses for the dominance fan.
3289
+ * `"legacy"` exists so CI can differentially prove the two equivalent (it is
3290
+ * the oracle in `band-collapse.test.ts`) and as a one-line rollback. It is an
3291
+ * internal knob — deliberately NOT on the exported `StyleGeneratorOptions`,
3292
+ * so it cannot reach `@chrtco/sdk` consumers via `ChrtStyleOptions`.
3356
3293
  */
3357
- declare function getMaxCellsForZoom(zoom: number): number;
3294
+ type BandFanMode = "collapsed" | "legacy";
3358
3295
 
3359
3296
  /**
3360
- * IHO S-52 / NOAA Chart No. 1 / INT1 nautical symbol SVGs.
3361
- *
3362
- * Complete symbology suite following:
3363
- * - IHO S-52 Presentation Library (Ed 4.0)
3364
- * - IALA Maritime Buoyage System R1001 (Region B for US waters)
3365
- * - NOAA Chart No. 1 (Ed 13) / INT1
3366
- *
3367
- * Buoy shape terminology (US / Chart No. 1):
3368
- * - CAN (Q20): Flat-top cylindrical buoy — Port mark in IALA-B (red)
3369
- * - NUN (Q21): Conical/pointed-top buoy — Starboard mark in IALA-B (green)
3370
- * - PILLAR: Tall narrow buoy — used for cardinal, isolated danger, etc.
3371
- * - SPAR: Tall thin pole buoy
3372
- * - SPHERICAL: Round buoy — safe water marks
3373
- * - BARREL: Barrel/tun shaped buoy
3374
- * - SUPER: Large navigation buoy / LANBY
3375
- *
3376
- * Cardinal mark topmark orientations (IALA):
3377
- * - North: both cones point UP ▲▲
3378
- * - South: both cones point DOWN ▼▼
3379
- * - East: cones base-to-base (diamond) ◆
3380
- * - West: cones point-to-point (hourglass) ⊗
3381
- *
3382
- * Each symbol is a compact SVG at 32×32 for Mapbox GL `map.addImage()`.
3297
+ * Generates a complete Mapbox GL Style Specification v8 JSON
3298
+ * from the S-52 atlas definitions.
3383
3299
  */
3384
- /** Color used in the SVG with its S-52 token name, hex, and usage */
3385
- interface SymbolColor {
3386
- token: string;
3387
- hex: string;
3388
- usage: string;
3389
- }
3390
- /** Line style used in the SVG */
3391
- interface SymbolLineStyle {
3392
- element: string;
3393
- width: number;
3394
- pattern?: string;
3395
- color: string;
3396
- }
3397
- /** Fill element used in the SVG */
3398
- interface SymbolFillElement {
3399
- element: string;
3400
- color: string;
3401
- opacity?: number;
3402
- }
3403
- interface NauticalSymbol {
3404
- id: string;
3405
- label: string;
3406
- /** INT1 / Chart No. 1 reference */
3407
- int1Ref: string;
3408
- /** Chart No. 1 section letter */
3409
- section: string;
3410
- getSvg: () => string;
3411
- /** Generate SVG with theme-specific colors (for bridge icons etc.) */
3412
- getThemedSvg?: (chblk: string, movable: string) => string;
3413
- /** What this symbol represents on a chart */
3414
- description?: string;
3415
- /** S-52 Presentation Library symbol name */
3416
- s52PlRef?: string;
3417
- /** S-57 object class (e.g. "LNDMRK") when it can't be derived from s52PlRef's prefix */
3418
- s57Class?: string;
3419
- /** Grouping key for family clustering */
3420
- group?: string;
3421
- /** IDs of related variant symbols */
3422
- variants?: string[];
3423
- /** S-57 attribute that selects this variant */
3424
- attributeKey?: string;
3425
- /** Value of that attribute for this variant */
3426
- attributeValue?: number;
3427
- /** Example label strings seen on charts */
3428
- labelExamples?: string[];
3429
- /** What the color represents */
3430
- colorMeaning?: string;
3431
- /** Colors used in the SVG */
3432
- colors?: SymbolColor[];
3433
- /** Line/stroke styles used in the SVG */
3434
- lineStyles?: SymbolLineStyle[];
3435
- /** Fill regions in the SVG */
3436
- fillElements?: SymbolFillElement[];
3437
- /** Expanded navigational usage context (2-4 sentences) */
3438
- usageContext?: string;
3439
- /** Render size for documentation */
3440
- renderSize?: string;
3441
- }
3442
- /** A logical grouping of related symbols */
3443
- interface SymbolGroup {
3444
- id: string;
3445
- label: string;
3446
- section: string;
3447
- description: string;
3448
- symbolIds: string[];
3300
+
3301
+ interface MapboxStyle {
3302
+ version: 8;
3303
+ name: string;
3304
+ metadata: Record<string, unknown>;
3305
+ sources: Record<string, unknown>;
3306
+ sprite?: string;
3307
+ glyphs: string;
3308
+ layers: StyleLayer[];
3449
3309
  }
3450
- /** Chart No. 1 section metadata */
3451
- interface ChartSection {
3452
- id: string;
3453
- label: string;
3454
- description: string;
3310
+ interface StyleGeneratorOptions extends BuildSourcesOptions {
3311
+ /**
3312
+ * When set, emit a "paper chart" style centered on the selected scale
3313
+ * band, with graceful coverage fallback: the requested band's layers
3314
+ * draw on top where its cells have data; coarser bands (driven by
3315
+ * `CHART_CASCADE_CODES`) fill gaps where the requested band has no
3316
+ * cells. Per-feature SCAMIN minzooms are preserved on every cascade
3317
+ * variant; `maxzoom` is dropped so the user can zoom in indefinitely.
3318
+ *
3319
+ * When omitted (default), continuous mode emits one layer copy per
3320
+ * source band with strict per-band zoom windows so exactly one band
3321
+ * renders at any given continuous zoom.
3322
+ *
3323
+ * `lockedBounds` (optional) restricts chart-mode rendering to a
3324
+ * specific bbox via a `["within"]` filter — applied ONLY to the
3325
+ * requested band's variants. Cascade fallback bands render across the
3326
+ * full viewport so a chart-Harbor user panning offshore still sees
3327
+ * Approach/Coastal data. The frontend pairs this with a translucent
3328
+ * grey overlay outside the bbox so the lock boundary is still visible
3329
+ * to the user (the fallback data is dimmed by the overlay but not
3330
+ * filtered out).
3331
+ */
3332
+ chartMode?: {
3333
+ band: ScaleBand$1;
3334
+ lockedBounds?: BBox;
3335
+ };
3336
+ /**
3337
+ * Display unit for depth labels (SOUNDG soundings, DEPCNT contour
3338
+ * values). Source data is always meters (S-57 / VALSOU / VALDCO);
3339
+ * "feet" wraps the text-field expression with a unit conversion +
3340
+ * round so the rendered numbers match. Default "meters".
3341
+ */
3342
+ depthUnits?: "meters" | "feet";
3343
+ /**
3344
+ * Which symbol artwork the style references. "s52" (default) uses the
3345
+ * PresLib-derived S-52 set; "s101" rewrites icon references to the
3346
+ * official S-101 Portrayal Catalogue symbols (s101: image namespace)
3347
+ * where a counterpart exists — see s101-style.ts. "chrt" applies the
3348
+ * CHRT house ATON/LIGHTS glyphs with colour-true tinting — see
3349
+ * chrt-style.ts. Selection logic differences are documented per module.
3350
+ */
3351
+ symbolSet?: "s52" | "s101" | "chrt" | "raster";
3352
+ /**
3353
+ * The mariner's safety contour in metres — the depth below which a
3354
+ * feature is "dangerous to my hull". Drives both safety-contour
3355
+ * conditional symbology rules:
3356
+ *
3357
+ * - §SOUNDG02 — soundings shoaler than this render in the
3358
+ * high-contrast color; deeper soundings render in the muted color.
3359
+ * - §CSYM02 — isolated dangers (rocks / wrecks / obstructions)
3360
+ * shoaler than this get the DNGCIR01 danger ring.
3361
+ *
3362
+ * Default `DEFAULT_SAFETY_CONTOUR_M` (10 m).
3363
+ *
3364
+ * Does not affect the DEPARE depth-area shading, whose 2/5/10 m bands
3365
+ * are a separate S-52 concept.
3366
+ */
3367
+ safetyContour?: number;
3368
+ /**
3369
+ * When false, all soundings render in a single shade. Default true.
3370
+ */
3371
+ twoShadeSoundings?: boolean;
3372
+ /**
3373
+ * Label model for the 9 orientation place-name classes (SEAARE, LNDRGN,
3374
+ * LAKARE, RIVERS, CANALS, BUAARE, AIRARE, LNDARE, PRDARE).
3375
+ *
3376
+ * "legacy" (default) — the per-cell-band orientation fan
3377
+ * (fanOrientationLabelByCellBand): one label per feature per band,
3378
+ * deconflicted only by `_label_anchor` tags and watermark opacity.
3379
+ *
3380
+ * "placename" — 2 PLACENAME symbol layers over the build-side
3381
+ * place-name index (CHRTbaremetal placeNameIndex.ts): one collision-
3382
+ * managed label per named place with a per-feature zoom window; the
3383
+ * orientation fan for those classes is NOT emitted. Requires tiles
3384
+ * built with the PLACENAME side index — on older tiles the layers
3385
+ * simply draw nothing (soft-fail is index-side), which is why the
3386
+ * default stays "legacy" until L2c's rebuild + Maine-coast visual
3387
+ * verification flips it (docs/design/chart-label-deconfliction.md
3388
+ * §2.3). Feature-label classes are untouched either way.
3389
+ */
3390
+ labels?: "legacy" | "placename";
3391
+ /**
3392
+ * Font weight for SOUNDG labels. Default "semibold" — crisper numerals
3393
+ * at small sizes on screen.
3394
+ */
3395
+ soundingFontWeight?: "regular" | "semibold";
3396
+ /**
3397
+ * How big the depth figures are drawn — soundings, contour values and the
3398
+ * depth beside a rock, wreck or obstruction. "standard" (default) is
3399
+ * byte-identical to omitting it; "large" enlarges those labels only, and
3400
+ * never their text, colours, sort keys, filters or layer set. See
3401
+ * depth-label-size.ts.
3402
+ */
3403
+ depthLabelSize?: DepthLabelSize;
3455
3404
  }
3456
- declare const NAUTICAL_SYMBOLS: NauticalSymbol[];
3457
- /** S-57 object class → default symbol ID mapping.
3458
- * Values are S-52 PL symbol names (matching SVG filenames in s52/point/).
3459
- * Conditional expressions in getIconExpression() override these for classes
3460
- * with attribute-dependent variants.
3461
- */
3462
- declare const SYMBOL_MAP: Record<string, string>;
3463
- /** Get the Mapbox icon-image expression for data-driven symbol selection.
3464
- * Returns S-52 PL symbol names that correspond to SVG files in s52/point/.
3405
+ declare function generateStyle(palette?: ColorTableName, opts?: StyleGeneratorOptions & {
3406
+ bandFan?: BandFanMode;
3407
+ }): MapboxStyle;
3408
+ declare function generateStyleJSON(palette?: ColorTableName, opts?: StyleGeneratorOptions): string;
3409
+ /** Get summary stats about the generated style */
3410
+ declare function getStyleStats(style: MapboxStyle): {
3411
+ totalLayers: number;
3412
+ fillLayers: number;
3413
+ lineLayers: number;
3414
+ symbolLayers: number;
3415
+ circleLayers: number;
3416
+ sources: number;
3417
+ objectClasses: Set<string>;
3418
+ };
3419
+
3420
+ /**
3421
+ * The e-ink portrayal pass — what `EINK_COLOR`, `EINK_MONO` and `EINK_NIGHT`
3422
+ * change beyond their colour tables. Night is mono inverted: everything below
3423
+ * applies, the patterns and safety contour are drawn LIGHT (the patterns are
3424
+ * themed per palette, the contour takes the table's DEPSC), and the chart
3425
+ * background gets the no-coverage crosshatch, because a black no-data hole
3426
+ * on a black-water page would read as deep water.
3427
+ *
3428
+ * A colour table can only move colours. E-paper needs more than that (owner
3429
+ * decision 2026-10-05: CHRT serves the e-ink chart, clients never recolour it;
3430
+ * requirements in CHRTsail's e-ink product spec §3c):
3431
+ *
3432
+ * 1. MEANING UNCHANGED. Nothing here adds, removes or reorders a feature
3433
+ * class or changes a filter that decides WHICH features draw. It changes
3434
+ * how they look, and adds two kinds of OVERLAY layer (depth patterns,
3435
+ * the safety contour) that draw features the chart already draws.
3436
+ * 2. MONO STANDS ON ITS OWN. Depth steps become fill PATTERNS (mono only);
3437
+ * danger areas get a cross-hatch; the safety contour gets its own heavy
3438
+ * line; restricted/caution areas keep their dashes, now heavier.
3439
+ * 3. DEPTH IN AT MOST FOUR STEPS. The tables do the colour side (DEPMD ==
3440
+ * DEPDW). Mono shows three: drying (hatch), shallower than the safety
3441
+ * contour (stipple), safe (paper).
3442
+ * 4. SAFETY CONTOUR AND DANGERS EMPHASISED: a heavy black safety-contour
3443
+ * line; danger symbols drawn larger; danger areas cross-hatched.
3444
+ * 5. COVERAGE PORTRAYAL SURVIVES: the no-coverage crosshatch goes to full
3445
+ * strength rather than being dropped with the other translucency.
3446
+ * 6. ATTRIBUTION/CURRENCY METADATA: untouched — this pass never edits
3447
+ * `sources` or the metadata other than adding `chrt:eink`.
3448
+ * 7. SIZES FOR 150 ppi COLOUR / 300 ppi MONO: every line ×1.5 (min 1 px),
3449
+ * text ×1.15 with a solid paper halo, no translucency, no blur, no fades.
3450
+ *
3451
+ * Translucency is the thing e-paper cannot draw: a 10% magenta wash over pale
3452
+ * blue is a fifth grey in mono and nothing at all on Kaleido 3. So faint washes
3453
+ * (< 0.3) are dropped — every one of them is a CHRT addition over a feature
3454
+ * whose boundary and symbol S-52 already draws — and stronger ones are made
3455
+ * solid.
3456
+ *
3457
+ * Pure: takes a generated style, returns a new one. `generateStyle` applies it
3458
+ * for the e-ink tables, so the served style, the offline style and the SDK's
3459
+ * local `generateStyle` all carry the same portrayal.
3465
3460
  */
3466
- declare function getIconExpression(layerName: string): string | unknown[];
3461
+
3462
+ /** The three e-ink fill patterns (house glyphs, registered with the world hatches). */
3463
+ declare const EINK_DOTS_PATTERN = "chrt:eink-dots";
3464
+ declare const EINK_HATCH_PATTERN = "chrt:eink-hatch";
3465
+ declare const EINK_DANGER_PATTERN = "chrt:eink-danger";
3466
+ /** The VALDCO values that ARE the safety contour, one per ladder. */
3467
+ declare function safetyContourValues(safetyContourM: number): number[];
3468
+ interface EinkPortrayalOptions {
3469
+ /** The mariner's safety contour, metres — the same value the generator used. */
3470
+ safetyContour?: number;
3471
+ }
3467
3472
  /**
3468
- * Browser-only image loaders (`loadSymbolImages`, `loadThemedBridgeImages`)
3469
- * live in `./symbols-browser.ts` (sub-export `@chrtco/sdk/atlas/browser`) —
3470
- * they require a DOM (use `new Image()` + `map.addImage()`).
3473
+ * Apply the e-ink portrayal for `palette`. Returns the style unchanged for a
3474
+ * non-e-ink palette, so a caller may apply it unconditionally.
3471
3475
  */
3472
- /** Get inline SVG string for a symbol by ID (for the Atlas legend) */
3473
- declare function getSymbolSvgString(symbolId: string): string | null;
3474
- declare const CHART_SECTIONS: ChartSection[];
3475
- declare const SYMBOL_GROUPS: SymbolGroup[];
3476
- /** Generate a single SVG sprite sheet containing all symbols as <symbol> elements */
3477
- declare function generateSpriteSheet(): string;
3478
- /** Generate a JSON manifest mapping symbol IDs to metadata */
3479
- declare function generateSpriteManifest(): Record<string, {
3480
- label: string;
3481
- int1Ref: string;
3482
- section: string;
3483
- group?: string;
3484
- s52PlRef?: string;
3485
- description?: string;
3486
- attributeKey?: string;
3487
- attributeValue?: number;
3488
- }>;
3476
+ declare function applyEinkPortrayal(style: MapboxStyle, palette: ColorTableName, opts?: EinkPortrayalOptions): MapboxStyle;
3489
3477
 
3490
3478
  /**
3491
- * IHO S-101 ENC Product Specification — Feature Catalogue
3479
+ * Light arithmetic for the chart palettes: how much light a colour emits, and
3480
+ * how to dim a colour without changing its hue.
3492
3481
  *
3493
- * S-101 replaces S-57 as the data model for Electronic Navigational Charts.
3494
- * This catalogue maps S-101 feature classes to:
3495
- * - S-57 equivalents (where they exist)
3496
- * - Chart No. 1 sections (same organizational structure)
3497
- * - Portrayal catalogue references (S-100 Portrayal Model)
3482
+ * Everything here works in LINEAR light (WCAG 2.x relative luminance, sRGB
3483
+ * primaries). That is the quantity a dark-adapted eye responds to, and it is
3484
+ * the one `sounding-legibility.test.ts` already measures soundings in. Scaling
3485
+ * the sRGB-ENCODED channels instead would dim dark colours far more than
3486
+ * bright ones and shift every hue it touched.
3498
3487
  *
3499
- * Key S-101 changes from S-57:
3500
- * - CamelCase feature class names (DepthArea vs DEPARE)
3501
- * - Complex/structured attributes (nested objects vs flat)
3502
- * - New feature types for AIS, quality, under-keel clearance
3503
- * - Removed/merged features for simplification
3488
+ * The one derivation the palettes rely on, and the reason this file exists:
3489
+ * NIGHT's deeper sounding SNDG1 (#897350) is SNDG2 (#c8aa78) at 43% of its
3490
+ * linear light. `labels.ts` builds its NIGHT secondary text the same way, so
3491
+ * the two stay one family by construction rather than by two hand-typed hexes
3492
+ * that happen to agree.
3493
+ */
3494
+ /** WCAG 2.x relative luminance of a `#rrggbb` colour, 0 (black) to 1 (white). */
3495
+ declare function relativeLuminance(hex: string): number;
3496
+ /** WCAG 2.x contrast ratio between two `#rrggbb` colours, 1 to 21. */
3497
+ declare function contrastRatio(a: string, b: string): number;
3498
+ /**
3499
+ * The same hue at `factor` times its linear light. `factor` above 1 can clip a
3500
+ * channel, which shifts the hue; nothing in the palettes brightens, so callers
3501
+ * pass 0–1.
3502
+ */
3503
+ declare function scaleLinearLight(hex: string, factor: number): string;
3504
+ /**
3505
+ * `hex` dimmed, hue kept, until its luminance is at most `ceiling`. A colour
3506
+ * already at or under the ceiling comes back unchanged (lower-cased).
3507
+ */
3508
+ declare function capLuminance(hex: string, ceiling: number): string;
3509
+ /**
3510
+ * NIGHT's two ink levels, shared by every family's text and marks.
3504
3511
  *
3505
- * Reference: IHO S-101 Product Specification (Ed 1.1.0)
3506
- * IHO S-100 Universal Hydrographic Data Model
3512
+ * `NIGHT_INK` S52_NIGHT.CHBLK — the palette's linework and navigation
3513
+ * text. The brightest thing NIGHT draws on purpose.
3514
+ * `NIGHT_MUTED_INK` that ink at 43% of its linear light, which is how
3515
+ * S52_NIGHT.SNDG1 (the deeper soundings) was derived.
3516
+ * Everything that orients rather than navigates is drawn
3517
+ * at or under this, and `night-luminance.test.ts` holds
3518
+ * every visible NIGHT colour to it unless an exemption
3519
+ * names a reason.
3507
3520
  */
3508
- interface S101Feature {
3509
- /** S-101 camelCase feature class name */
3510
- featureClass: string;
3511
- /** Human-readable name */
3512
- name: string;
3513
- /** S-57 6-char equivalent acronym, if any */
3514
- s57Equivalent?: string;
3515
- /** Chart No. 1 section letter */
3516
- section: string;
3517
- /** Group key for clustering */
3518
- group: string;
3519
- /** Feature category */
3520
- category: "geo" | "meta" | "info";
3521
- /** Description of the feature */
3522
- description: string;
3523
- /** S-100 Portrayal Catalogue reference */
3524
- portrayalRef?: string;
3525
- /** True if this feature has no S-57 equivalent */
3526
- isNewInS101: boolean;
3527
- /** Key attributes specific to this feature */
3528
- attributes?: string[];
3529
- }
3530
- declare const S101_FEATURES: S101Feature[];
3531
- declare const S101_CHART_SECTIONS: {
3532
- id: string;
3533
- label: string;
3534
- description: string;
3535
- }[];
3536
- declare const S101_SYMBOL_GROUPS: {
3537
- id: string;
3538
- label: string;
3539
- section: string;
3540
- description: string;
3541
- }[];
3521
+ declare const NIGHT_INK: string;
3522
+ declare const NIGHT_MUTED_INK: string;
3542
3523
 
3543
3524
  /**
3544
- * S-101 Portrayal Catalogue — New Symbol SVGs
3525
+ * Is this MapLibre map still there to be written to?
3545
3526
  *
3546
- * These symbols represent features introduced in S-101 that have no
3547
- * direct S-57/S-52 equivalent. The existing S-52 symbols are reused
3548
- * where S-101 retains visual continuity with the S-57 portrayal.
3527
+ * ===========================================================================
3528
+ * ⚠ ASK AFTER EVERY AWAIT. A MAP CAN BE REMOVED WHILE YOU WERE WAITING.
3529
+ * ===========================================================================
3549
3530
  *
3550
- * Reference: IHO S-101 Portrayal Catalogue (Ed 1.1.0)
3531
+ * `map.remove()` deletes `map.style`, and after that nearly every method that
3532
+ * touches the style throws rather than returning: `hasImage`, `addImage`,
3533
+ * `updateImage`, `getSource`, `addLayer` all fail with "Cannot read properties
3534
+ * of undefined (reading 'getImage')" and its siblings. Code that was handed a
3535
+ * map, awaited a decode, an IndexedDB read or a fetch, and then wrote to the
3536
+ * map, throws from wherever it resumed — usually a promise nobody is awaiting,
3537
+ * so the throw is an uncaught error with no caller to explain it.
3538
+ *
3539
+ * Measured in CHRTdesktop (chip 63, 2026-10-04): with the page's main thread
3540
+ * held at 6× CPU, the chart removed its map in the middle of a pan, and the
3541
+ * S-52 image loader resumed onto it:
3542
+ *
3543
+ * TypeError: Cannot read properties of undefined (reading 'getImage')
3544
+ * at Map.hasImage
3545
+ * at addImageToMap (s52-image-cache.ts)
3546
+ *
3547
+ * The rule, for anything in this package or the app that takes a map: after
3548
+ * an `await` (or in a `.then`, a frame callback, an `onload`), check this
3549
+ * before the first call on the map, and stop quietly if it says no. A removed
3550
+ * map is not an error to report — whoever removed it is building the next one.
3551
+ *
3552
+ * ⚠ `_removed`, NOT `style`. maplibre sets `_removed = true` as the last step
3553
+ * of `remove()` (typed on `Map` in maplibre-gl 4 and 5), so it is the fact,
3554
+ * where `style === undefined` is a consequence. Test doubles that never had a
3555
+ * `style` read as live, which is what they are. The app pins the maplibre side
3556
+ * of this (`map-live.test.ts`), so an upgrade that renames the field fails a
3557
+ * test rather than silently making every check say yes.
3558
+ *
3559
+ * Typed `object` so the structural map shapes the app passes around (a tile
3560
+ * census, an errored-tile tracker) can be asked too, not only `maplibregl.Map`.
3551
3561
  */
3562
+ declare function isMapLive(map: object): boolean;
3552
3563
 
3553
- declare const S101_NEW_SYMBOLS: NauticalSymbol[];
3564
+ /** S-52 display categories – controls default visibility */
3565
+ type DisplayCategory = "BASE" | "STANDARD" | "OTHER" | "MARINERS" | "META";
3566
+ /** Mapbox GL layer type we'll use for this entry */
3567
+ type GeomType = "fill" | "line" | "circle" | "symbol";
3568
+ /** Nautical groupings (loosely follows Chart No. 1 sections) */
3569
+ type NauticalCategory = "Depths" | "Land" | "Navigation Aids" | "Hazards" | "Regulated Areas" | "Infrastructure" | "Meta";
3570
+ /** Paint properties – a subset we actually use */
3571
+ interface AtlasStyle {
3572
+ type: GeomType;
3573
+ paint: Record<string, unknown>;
3574
+ layout?: Record<string, unknown>;
3575
+ /** Nautical symbol ID for symbol-type layers */
3576
+ iconImage?: string;
3577
+ }
3578
+ /** One row in the S-52 lookup table */
3579
+ interface LookupEntry {
3580
+ /** S-57 object class acronym */
3581
+ objectClass: string;
3582
+ /** Human-readable name */
3583
+ name: string;
3584
+ /** Chart No. 1 section */
3585
+ category: NauticalCategory;
3586
+ /** S-52 display category */
3587
+ displayCategory: DisplayCategory;
3588
+ /** Default style using S-52 color tokens (resolved later) */
3589
+ style: AtlasStyle;
3590
+ /** Whether conditional symbology may override the style */
3591
+ hasConditional?: boolean;
3592
+ /** Attributes that affect styling */
3593
+ conditionalAttributes?: string[];
3594
+ /** INT1 / Chart No. 1 reference (e.g. "Q130.1") */
3595
+ int1Ref?: string;
3596
+ /** S-52 viewing group number */
3597
+ viewingGroup?: number;
3598
+ /** Minimum compilation scale for display (SCAMIN) */
3599
+ scamin?: number;
3600
+ /**
3601
+ * Mark this class as deliberately invisible (label-only or metadata-only)
3602
+ * so the layer-coverage audit doesn't flag its geometry-type gaps as bugs.
3603
+ *
3604
+ * Examples:
3605
+ * - SOUNDG: rendered as depth numbers via labels.ts, not as visible dots
3606
+ * - LNDRGN: region NAME labels only, no shape
3607
+ * - M_NPUB / M_NSYS / M_QUAL / M_SREL / DSID: S-57 metadata classes;
3608
+ * present for click-query attributes, never drawn
3609
+ *
3610
+ * Without this flag, `scripts/audit-layer-coverage.ts` reports the
3611
+ * thousands of features in these classes as "gaps" every week even
3612
+ * though the invisibility is intentional.
3613
+ */
3614
+ auditIgnore?: boolean;
3615
+ }
3616
+ /** Resolved style ready for Mapbox GL (tokens → hex) */
3617
+ interface ResolvedStyle {
3618
+ type: GeomType;
3619
+ paint: Record<string, unknown>;
3620
+ layout?: Record<string, unknown>;
3621
+ minzoom?: number;
3622
+ maxzoom?: number;
3623
+ }
3554
3624
 
3555
3625
  /**
3556
- * Post-consolidation (2026-04): one consolidated PMTiles per region, all
3557
- * S-57 classes as distinct MVT layers inside. The MapLibre style emits one
3558
- * vector source (id = region) and N paint layers that reference it via
3559
- * `source-layer` = S-57 class name.
3560
- *
3561
- * `buildSources()` returns either the legacy Mapbox MTS shape (preserved for
3562
- * demos / external style export — see generateStyle comments) or the PMTiles
3563
- * shape pointing at a single tile URL per region.
3626
+ * S-52 lookup table covering all S-57 object classes found in NOAA ENCs.
3627
+ * Colours reference S-52 tokens (resolved at render time).
3628
+ * INT1 references per NOAA Chart No. 1 (Ed 13).
3564
3629
  */
3565
- /** Vector source emitted for the legacy Mapbox MTS path. */
3566
- interface MapboxVectorSource {
3567
- type: "vector";
3568
- url: string;
3569
- }
3630
+ declare const S52_LOOKUP: Record<string, LookupEntry>;
3631
+
3570
3632
  /**
3571
- * Vector source emitted for the tiles.chrt.co PMTiles path. Uses an explicit
3572
- * `tiles` array (not a TileJSON `url`) because the tile-worker serves raw MVT
3573
- * bytes without a TileJSON descriptor.
3633
+ * S-52 draw order: layers listed bottom → top.
3634
+ * Layers not in this list are inserted before point features.
3574
3635
  */
3575
- interface PmtilesVectorSource {
3576
- type: "vector";
3577
- tiles: string[];
3578
- minzoom: number;
3579
- maxzoom: number;
3580
- /** The credit a renderer shows for this source (MapLibre's attribution control). */
3581
- attribution?: string;
3582
- }
3636
+ declare const S52_DRAW_ORDER: string[];
3637
+ /** Get numeric draw priority for a layer (lower = drawn first / below) */
3638
+ declare function getDrawPriority(layerName: string): number;
3639
+
3640
+ /** S-52 display modes */
3641
+ type DisplayMode = "BASE" | "STANDARD" | "ALL";
3642
+ /** Check if a layer's display category is visible in the given mode */
3643
+ declare function isVisibleInMode(displayCategory: DisplayCategory, mode: DisplayMode): boolean;
3583
3644
  /**
3584
- * Terrarium elevation, served as PNG from `CHRT_RELIEF`. Separate from the
3585
- * vector shapes because MapLibre needs `type: "raster-dem"` and an explicit
3586
- * `encoding` — a raster-dem source read as a plain raster renders the encoded
3587
- * RGB as a garish false-colour image rather than as elevation.
3645
+ * Convert S-52 SCAMIN (scale minimum) to Mapbox GL minzoom.
3588
3646
  */
3589
- interface RasterDemSource {
3590
- type: "raster-dem";
3591
- tiles: string[];
3592
- encoding: "terrarium";
3593
- tileSize: number;
3594
- minzoom: number;
3595
- maxzoom: number;
3596
- /** The credit a renderer shows for this source (MapLibre's attribution control). */
3597
- attribution?: string;
3598
- }
3599
- type VectorSourceSpec = MapboxVectorSource | PmtilesVectorSource;
3600
- type AnySourceSpec = VectorSourceSpec | RasterDemSource;
3601
- /** CHRTland's vector land base. ADR 0023: its own archive, never chart data. */
3602
- declare const LAND_REGION = "CHRT_LAND";
3603
- /** CHRTland's terrarium relief. */
3604
- declare const RELIEF_REGION = "CHRT_RELIEF";
3605
- /** The world basemap under everything (completing-the-world-map, 2026-08-27). */
3606
- declare const BASE_REGION = "CHRT_BASE";
3647
+ declare function scaminToMinZoom(scamin?: number): number | undefined;
3648
+ declare function getCollisionSettings(viewingGroup?: number): {
3649
+ iconAllowOverlap: boolean;
3650
+ iconOptional: boolean;
3651
+ symbolSortKey?: number;
3652
+ };
3607
3653
  /**
3608
- * CHRT_BASE's zoom range. 11 is the deepest zoom the archive's detail tier
3609
- * carries honestly — the planet's inland water exists from z9 and is read at
3610
- * z10 — and the world tier is tiled to the same depth so no layer vanishes
3611
- * from a tile the detail tier creates. Above 11 the client overzooms, and
3612
- * overzoomed inland water looks soft DELIBERATELY: it is "roughly where the
3613
- * lakes are", not a surveyed shoreline. Must equal the builder's MAXZOOM
3614
- * (tools/chrtbase-build/build.mjs).
3654
+ * Get the resolved Mapbox GL style for a given S-57 layer.
3615
3655
  */
3616
- declare const BASE_MINZOOM = 0;
3617
- declare const BASE_MAXZOOM = 11;
3656
+ declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName, opts?: {
3657
+ safetyContour?: number;
3658
+ }): ResolvedStyle;
3659
+ declare function getLookupEntry(layerName: string): LookupEntry;
3660
+
3618
3661
  /**
3619
- * The credit the ONLINE CHRT_BASE source carries (owner wording, 2026-09-16).
3620
- *
3621
- * OpenStreetMap is named because the online archive's z9–11 detail tier IS
3622
- * OSM-derived inland water (ODbL, `public/chrtbase-derivation.json`). The
3623
- * app-embeddable offline bundle is a z0–6 cut that contains no OSM data and
3624
- * carries `BASE_BUNDLE_ATTRIBUTION` instead (base-layers.ts) — one credit per
3625
- * artefact, each naming exactly the data it holds.
3662
+ * S-52 Conditional Symbology Procedures.
3663
+ * Each function takes feature properties and returns paint overrides (hex colours).
3664
+ * All expression builders accept an optional colorTable parameter.
3626
3665
  */
3627
- declare const BASE_ATTRIBUTION = "\u00A9 CHRT \u00B7 Made with Natural Earth \u00B7 \u00A9 OpenStreetMap contributors";
3628
3666
  /**
3629
- * The credit the CHRT_LAND source carries. CHRTland's land, land use, roads,
3630
- * places, peaks and marinas are OpenStreetMap via the Protomaps planet, and the
3631
- * ODbL requires this notice wherever the map is shown (.chrt/legal.md
3632
- * "Required notices"). Its other inputs — the ENC's charted land, USGS 3DEP
3633
- * contours, GNIS names — are US federal public domain and owe none. Same
3634
- * string as the offline land bundle's (offline-worker `LAND_PROVENANCE`).
3667
+ * Default safety contour in metres — the depth below which a feature is
3668
+ * "dangerous to my hull" for portrayal purposes.
3669
+ *
3670
+ * This is the single source of truth for the *default* used by every
3671
+ * safety-contour-driven conditional symbology rule, so the rules can't
3672
+ * silently disagree with each other:
3673
+ *
3674
+ * - §SOUNDG02 — soundings shoaler than the contour get the
3675
+ * high-contrast shade (see `getLabelConfig` in labels.ts).
3676
+ * - §CSYM02 — isolated dangers shoaler than the contour get the
3677
+ * DNGCIR01 ring (see `buildLayersForClass` in layer-builder.ts).
3678
+ * - §DEPARE04 — the MS/MD shade boundary (owner decision 2026-08-21,
3679
+ * replacing a hardcoded 5 m middle cut; see the DEPARE block below
3680
+ * for the full IHO correspondence).
3681
+ *
3682
+ * All are overridden by the user's setting (sounding panel slider,
3683
+ * `safetyContour` in the style options); this is only the fallback when
3684
+ * none is supplied. It matches `custom-style.ts`'s config default.
3635
3685
  */
3636
- declare const LAND_ATTRIBUTION = "\u00A9 OpenStreetMap contributors";
3686
+ declare const DEFAULT_SAFETY_CONTOUR_M = 10;
3687
+ declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
3637
3688
  /**
3638
- * The credit the CHRT_RELIEF source carries. USGS 3DEP elevation, public
3639
- * domain (17 U.S.C. § 105) — no notice is required, so this is a courtesy that
3640
- * names the source. There is no OpenStreetMap in it.
3689
+ * The colour token of each `case` branch of `depareFillExpression`, in branch
3690
+ * order, then the fallback. `depth-area-opaque.ts` names the layers it splits
3691
+ * the expression into from this list, so the two read from one place.
3641
3692
  */
3642
- declare const RELIEF_ATTRIBUTION = "Elevation: USGS 3D Elevation Program";
3643
- /** Terrarium tiles are 512 px; the default 256 would halve the apparent relief. */
3644
- declare const RELIEF_TILE_SIZE = 512;
3693
+ declare const DEPARE_FILL_BRANCH_TOKENS: readonly ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
3694
+ declare const DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
3695
+ declare function depareFillExpression(ct?: ColorTableName, safetyContourM?: number): unknown[];
3696
+ declare function lightColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3697
+ declare function lightFillExpression(ct?: ColorTableName): unknown[];
3698
+ declare function lateralColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3699
+ declare function lateralFillExpression(ct?: ColorTableName): unknown[];
3700
+ declare function hazardColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3701
+ declare function hazardFillExpression(ct?: ColorTableName): unknown[];
3702
+ declare function daymarkColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3703
+
3645
3704
  /**
3646
- * How `CHRT_RELIEF`'s PNGs encode elevation. MapLibre's raster-dem default is
3647
- * `mapbox`, which decodes terrarium RGB into nonsense heights — so every
3648
- * surface that describes this source (this registry, the tile-worker's
3649
- * TileJSON) has to say it.
3705
+ * Label configurations for S-57 layers.
3706
+ * Defines which layers get text labels and how to extract label text.
3650
3707
  */
3651
- declare const RELIEF_ENCODING = "terrarium";
3652
- type SourceMode = "mapbox" | "pmtiles";
3708
+
3653
3709
  /**
3654
- * Query parameter carrying the resolved build id on every tile URL.
3655
- *
3656
- * ── Why the build has to be IN THE URL ────────────────────────────────────
3657
- *
3658
- * The Worker reports the build it served in the `X-Build-Id` RESPONSE HEADER
3659
- * and sends `Cache-Control: private, max-age=3600`. A header is not part of a
3660
- * cache key, so every build shares one URL: after a cutover, tiles the client
3661
- * already fetched keep coming from its own HTTP cache — the old build — while
3662
- * newly-panned-to tiles come from the new one, for up to the full hour. The
3663
- * client renders TWO BUILDS AT ONCE.
3710
+ * Feature-class hierarchy for SEAARE labels. Returns a tier number 1-5 per
3711
+ * feature:
3664
3712
  *
3665
- * On a nautical chart that is not a cosmetic seam. Measured live 2026-08-25,
3666
- * cutting `CHRT_LAND` from `463b265e` to `dbb69074` (a build that deliberately
3667
- * changes land extent): adjacent tiles disagreed about whether land was there,
3668
- * at the seam between them. Putting the build in the key gives each build its
3669
- * own cache namespace — old entries age out untouched, and the next style
3670
- * build flips the whole viewport at once instead of tile by tile.
3713
+ * Exported (with the other tier builders below) for the build-side parity
3714
+ * test: CHRTbaremetal replicates these taxonomies as data in
3715
+ * tools/chrt-baremetal/server/src/tilebuild/placeNameTiers.ts to stamp
3716
+ * `tier` onto PLACENAME index features, and
3717
+ * placename-tier-parity.test.ts evaluates the two against shared
3718
+ * fixtures. Change a branch here and that test names the drift.
3671
3719
  *
3672
- * ── Why a QUERY PARAMETER and not a path segment ──────────────────────────
3720
+ * 1 — Ocean (OBJNAM contains "<X> Ocean" — CATSEA has no
3721
+ * ocean code; the attribute is 1/unset on oceans)
3722
+ * 2 — Sea / Gulf (OBJNAM "Gulf of …" / "… Sea")
3723
+ * 3 — Bay / Sound / (CATSEA in {5 bay, 12 narrows} OR OBJNAM
3724
+ * Strait / Narrows " Bay" / " Sound" / " Strait")
3725
+ * 4 — Anchorage detail (OBJNAM contains "Anchorage Area")
3726
+ * 5 — Relief + default (CATSEA relief codes — authoritative — or
3727
+ * relief-term names, or anything uncategorised;
3728
+ * suppressed below z9 by the size curve)
3673
3729
  *
3674
- * A path segment would be the tidier-looking URL and it is the wrong trade
3675
- * here, for two reasons that are both about other people's code:
3730
+ * Order matters: tier-4/5 demotions (anchorages, relief) run BEFORE the
3731
+ * promotion shims so "Bear Seamount" (contains " Sea") and "…Anchorage
3732
+ * Area" compound names can't bubble up into headline tiers.
3733
+ * `coalesce(CATSEA, catsea)` handles post-tippecanoe case variation
3734
+ * (matches `s52-conditional.ts` precedent).
3735
+ */
3736
+ declare function buildSeaAreaTierExpression(): ExpressionSpecification;
3737
+ /**
3738
+ * Built-up area tier (BUAARE): major city / town / neighborhood.
3739
+ * 1 — Major city (CATBUA=1 urban, or OBJNAM length <= 8)
3740
+ * 2 — Town (CATBUA=5 village, or OBJNAM length <= 14)
3741
+ * 3 — Neighborhood (longer compound names)
3676
3742
  *
3677
- * 1. COMPATIBILITY IS PERMANENT, NOT "one release". iOS and desktop are
3678
- * prod-only clients that ship on their own cadence. The Worker routes on
3679
- * `url.pathname` (`TILE_ROUTE_PATH`) and reads only `?build=`, so a URL
3680
- * carrying this parameter routes BYTE-IDENTICALLY to one without it. A
3681
- * new path segment would instead need a second route regex kept alive
3682
- * indefinitely.
3683
- * 2. FIVE SEPARATE PATTERNS ALREADY MATCH THIS URL, and every one of them
3684
- * ends `(?:\?.*)?$` — `src/lib/map/tile-token-client.ts` TILE_URL_PATH
3685
- * and `src/site/chart/sail-tile-token.ts` (which decide whether a tile
3686
- * gets its Bearer), `packages/sdk/src/tile-auth.ts` TILE_URL_PATH,
3687
- * `public/chrt-tile-sw.js` TILE_URL_RE (which also reads the region out of
3688
- * the URL to pick its per-region cache), and
3689
- * `src/site/offline/pack-protocol.ts` TILE_URL (which rewrites a tile to
3690
- * a downloaded pack). Their optional PATH segment, by contrast, is
3691
- * `[a-z][a-z0-9-]*` — letter-first so it cannot be confused with the
3692
- * numeric `{z}` — and build ids are hex (`463b265e`, `4de67273`). A build
3693
- * in the path would miss every one of them, and a tile those patterns
3694
- * miss does not merely go uncached: it goes out with NO BEARER and comes
3695
- * back 401, which MapLibre records as a permanently errored tile. That is
3696
- * the exact failure the `.png` drift caused on staging-sail (see
3697
- * `chrt-tile-sw.js`).
3743
+ * Tier 1/2 may fire via CATBUA when populated; OBJNAM length is the
3744
+ * fallback so unpopulated CATBUA still produces a usable hierarchy.
3745
+ */
3746
+ declare function buildBuiltUpAreaTierExpression(): ExpressionSpecification;
3747
+ /**
3748
+ * Land region tier (LNDRGN): major region vs. local point.
3749
+ * 1 — Major region (OBJNAM length <= 12 AND no comma — "Long Island", "Cape Cod")
3750
+ * 2 — Local point (else — "Bird Island", "Cottage Park")
3698
3751
  *
3699
- * The query string is also what the caches this fix targets actually key on:
3700
- * the browser HTTP cache, MapLibre's own tile cache, and the Service Worker's
3701
- * `new Request(req.url)` entry key all include it.
3752
+ * No CATLND analog for region scope, so OBJNAM is the only signal.
3753
+ * The comma-absence test is a heuristic: NOAA compound names like
3754
+ * "Boston Harbor, MA Castle Island" carry a comma and belong at deeper
3755
+ * zoom regardless of length.
3756
+ */
3757
+ declare function buildLandRegionTierExpression(): ExpressionSpecification;
3758
+ /**
3759
+ * Land area tier (LNDARE): major vs. local.
3760
+ * 1 — Major (OBJNAM length <= 12)
3761
+ * 2 — Local
3702
3762
  *
3703
- * `v` rather than `build`: `?build=` is a taken name with a strict meaning on
3704
- * the Worker (`?build=candidate`, employee-only, 400 on any other value).
3763
+ * No category attribute. OBJNAM length is the only signal.
3764
+ */
3765
+ declare function buildLandAreaTierExpression(): ExpressionSpecification;
3766
+ /**
3767
+ * Lake tier (LAKARE): lake vs. reservoir/pond.
3768
+ * 1 — Lake (CATLAK=1, or OBJNAM length <= 12)
3769
+ * 2 — Reservoir/pond (CATLAK in {2,3}, or longer name)
3770
+ */
3771
+ declare function buildLakeTierExpression(): ExpressionSpecification;
3772
+ /**
3773
+ * River tier (RIVERS): major vs. local.
3774
+ * 1 — Major river (OBJNAM length <= 15 AND no comma)
3775
+ * 2 — Local creek (else)
3705
3776
  */
3706
- declare const BUILD_PARAM = "v";
3777
+ declare function buildRiverTierExpression(): ExpressionSpecification;
3707
3778
  /**
3708
- * One tile-URL template — the single place a CHRT tile URL is spelled.
3779
+ * Airport tier (AIRARE): major airport vs. small airstrip.
3780
+ * 1 — Major (OBJNAM length <= 25 — e.g. "Logan International Airport")
3781
+ * 2 — Airstrip
3782
+ */
3783
+ declare function buildAirportTierExpression(): ExpressionSpecification;
3784
+ interface LabelLayerConfig {
3785
+ /** Mapbox source layer type */
3786
+ type: "symbol";
3787
+ /** Text field expression or string */
3788
+ textField: ExpressionSpecification | string;
3789
+ /** Text size in pixels */
3790
+ textSize: number | ExpressionSpecification;
3791
+ /** Text color (hex) or data-driven expression */
3792
+ textColor: string | ExpressionSpecification;
3793
+ /** Halo color for contrast */
3794
+ textHaloColor: string;
3795
+ /** Halo width — pixels or zoom-interpolation expression */
3796
+ textHaloWidth: number | ExpressionSpecification;
3797
+ /** Text offset [x, y] in ems */
3798
+ textOffset: [number, number];
3799
+ /** Font stack */
3800
+ textFont?: string[];
3801
+ /** Additional layout properties */
3802
+ layout?: Record<string, unknown>;
3803
+ /** Minimum zoom level for this label */
3804
+ minzoom?: number;
3805
+ }
3806
+ /**
3807
+ * Optional palette-and-data-driven options for label config.
3709
3808
  *
3710
- * `buildId` is encoded rather than interpolated raw: MapLibre substitutes
3711
- * `{z}`/`{x}`/`{y}` by brace-matching the template, so an id containing a
3712
- * brace would corrupt every tile request. Empty / whitespace-only ids emit no
3713
- * parameter at all instead of a bare `?v=` that would key its own useless
3714
- * cache namespace.
3809
+ * Currently used by SOUNDG to pick palette-aware text/halo colors and to
3810
+ * drive the S-52 safety-contour two-shade rule. Other classes ignore
3811
+ * these options.
3715
3812
  */
3716
- declare function tileTemplate(base: string, region: string, extension: "mvt" | "png", buildId?: string): string;
3717
- /** Canonical source id used in `generateStyle()` — matches the region name
3718
- * of the one consolidated PMTiles served by the tile-worker for this region. */
3719
- declare const DEFAULT_REGION = "NOAA_ENC";
3720
- interface BuildSourcesOptions {
3721
- /** Which tile path to emit. Default: "pmtiles" (the supported path post-Mapbox). */
3722
- mode?: SourceMode;
3723
- /** Chart region. Default: "NOAA_ENC". */
3724
- region?: string;
3725
- /**
3726
- * Build id of the CHART region (`region`) — the `build_tag` a
3727
- * `mint-tile-token` response hands back.
3728
- *
3729
- * Emitted as the {@link BUILD_PARAM} query parameter on that region's tile
3730
- * template. See {@link tileTemplate} for why a build discriminator has to be
3731
- * in the URL at all.
3732
- *
3733
- * ⚠ THIS COVERS THE CHART REGION ONLY. A token is minted for one chart
3734
- * region and `CHRT_LAND` / `CHRT_RELIEF` ride it, so the JWT's `build_tag`
3735
- * says nothing about a land cutover — the same blind spot that made the
3736
- * Worker's edge-cache key wrong until `TILE_CACHE_VERSION` v3
3737
- * (cloudflare/tile-worker/src/cache-encoding.ts). Pass {@link buildTags} to
3738
- * discriminate the basemap sources too.
3739
- */
3740
- buildTag?: string;
3741
- /**
3742
- * Resolved build id per region — `{ NOAA_ENC: "…", CHRT_LAND: "…" }`.
3743
- *
3744
- * The complete form of {@link buildTag}: every source this style declares
3745
- * gets its own discriminator, so a `CHRT_LAND` promotion turns over the land
3746
- * URLs without touching the chart's (and vice versa). An entry here wins
3747
- * over `buildTag` for the same region.
3748
- *
3749
- * This is the mint response's `build_tags` map verbatim (claim contract
3750
- * v1.7) — `TileTokenResponse.buildTags` in both `lib/map/tile-token-client.ts`
3751
- * and the SDK's `tile-auth.ts`. The same map already keys the tile Service
3752
- * Worker's per-region caches; passing it here puts the same identity in the
3753
- * URL, so the layer above the Service Worker stops mixing builds too.
3754
- *
3755
- * A region absent from the map simply gets no discriminator — the URL it
3756
- * has always had. That is a deliberate soft edge: an older mint response, or
3757
- * a region with no servable build, must degrade to today's behaviour rather
3758
- * than emit a URL pinned to a build nobody resolved.
3759
- */
3760
- buildTags?: Readonly<Record<string, string>>;
3761
- /** Override tile-worker origin (e.g. staging). Default: DEFAULT_TILE_BASE_URL. */
3762
- tileBaseUrl?: string;
3813
+ interface LabelConfigOptions {
3814
+ /** S-52 colour table — drives palette-aware text and halo colors. */
3815
+ colorTable?: ColorTableName;
3763
3816
  /**
3764
- * Emit the CHRTland vector land base as a second source (ADR 0023). Off by
3765
- * default: it costs a source, and the region must be granted on the token.
3817
+ * Safety contour depth in metres. Soundings shoaler than this render in
3818
+ * `SNDG2` (high-contrast); deeper soundings render in `SNDG1` (muted).
3819
+ * S-52 §SOUNDG02 conditional symbology.
3766
3820
  */
3767
- land?: boolean;
3821
+ safetyContour?: number;
3768
3822
  /**
3769
- * Emit CHRTland's terrarium relief as a raster-dem source. Separate from
3770
- * `land` because relief needs a raster-capable worker AND a grant that
3771
- * `land-regions.ts` still withholds.
3823
+ * When false, all soundings render in `SNDG2` (single-shade). Default
3824
+ * true — emit the safety-contour case expression.
3772
3825
  */
3773
- relief?: boolean;
3826
+ twoShadeSoundings?: boolean;
3774
3827
  /**
3775
- * Emit the CHRT_BASE world basemap as a third vector source — the dark
3776
- * geography under everything, the tan jurisdiction fill, and the rough
3777
- * inland water inside countries CHRT carries. Its layers sit UNDER the
3778
- * no-coverage hatch's companions and everything else; see baseLayers().
3828
+ * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
3829
+ * numerals at small sizes. Default "semibold".
3779
3830
  */
3780
- base?: boolean;
3831
+ soundingFontWeight?: "regular" | "semibold";
3781
3832
  }
3782
3833
  /**
3783
- * Build the "sources" block for a CHRT style.
3834
+ * Two-tier label rendering strategy for the band-priority style.
3784
3835
  *
3785
- * - `mode: "mapbox"`: one mapbox:// url (kept for external style export only).
3786
- * - `mode: "pmtiles"` (default): one vector tile endpoint for the region,
3787
- * routed by the Worker to the one consolidated PMTiles:
3788
- * `${tileBaseUrl}/v1/tiles/{region}/{z}/{x}/{y}.mvt`
3836
+ * `feature-band-gated` — emit one variant per source band, each gated to
3837
+ * its band's zoom window (the existing `makeBandVariants` behaviour). Right
3838
+ * for dense feature labels where seeing the same buoy designation or
3839
+ * sounding twice across overlapping bands is visual noise.
3789
3840
  *
3790
- * Pass `buildTag` / `buildTags` to pin each source's URL to the build it is
3791
- * serving — without that, one URL is shared by every build and a cutover
3792
- * leaves the client rendering two of them at once. See {@link BUILD_PARAM}.
3841
+ * `orientation-cross-band` — emit a SINGLE label layer with no band
3842
+ * filter. The per-class `minzoom` is honoured. Right for orientation
3843
+ * "where am I" labels (sea-area names, land regions, cities) — these
3844
+ * must persist as the user zooms in, even if the broader-scope cell
3845
+ * carrying the OBJNAM is from a coarser band than the one rendering
3846
+ * features at the current zoom. Collision arbitration via
3847
+ * `ORIENTATION_BAND_SORT_KEY` gives the broader-scope (lower-band) copy
3848
+ * priority where the same OBJNAM exists in multiple bands.
3793
3849
  */
3794
- declare function buildSources(opts?: BuildSourcesOptions): Record<string, AnySourceSpec>;
3795
-
3850
+ type LabelBehavior = "feature-band-gated" | "orientation-cross-band";
3851
+ declare const LABEL_BEHAVIOR: Record<string, LabelBehavior>;
3852
+ /** Returns the rendering strategy for an S-57 class. Unknown classes default to band-gated. */
3853
+ declare function getLabelBehavior(layerName: string): LabelBehavior;
3796
3854
  /**
3797
- * DEPTH NUMBERS — how big the chart draws its depth figures.
3798
- *
3799
- * Asked for from the helm (chip 68, 2026-10-05), by a captain running CHRTsail
3800
- * desktop on a Raspberry Pi: *"I still need the depth numbers to be bolder
3801
- * which may be a setting. I just can't sail the boat and adjust all that stuff
3802
- * at the same time."* A set-and-forget choice, then, not a slider.
3803
- *
3804
- * ===========================================================================
3805
- * WHAT "LARGE" CHANGES, AND WHAT IT IS NOT ALLOWED TO TOUCH
3806
- * ===========================================================================
3855
+ * Class-based label priority ladder — THE single collision-priority model
3856
+ * for feature labels (portrayal-excellence P4). Converges the previous
3857
+ * per-mechanism patches (SAFETY_CRITICAL_LABELS allow-overlap, the
3858
+ * band-5/6-only Harbor collision discipline) into one explicit ranking.
3807
3859
  *
3808
- * It changes the SIZE of the glyphs (and, where a class was Regular, the
3809
- * WEIGHT, to the Semibold soundings already use), the halo a little — see
3810
- * `LARGE` for why less than the glyph — and the sounding collision padding —
3811
- * see `LARGE_SOUNDING_PADDING_PX` for why that one moves.
3860
+ * Rank semantics: LOWER rank = higher priority. The rank drives two
3861
+ * mechanisms that must stay in agreement:
3812
3862
  *
3813
- * It does not touch, and `depth-label-size.test.ts` holds every one of these
3814
- * equal to the Standard style in every palette and every symbol family:
3863
+ * 1. `symbol-sort-key` — every feature label carries
3864
+ * `rank*1000 + within-class-key`, so if label layers are ever merged
3865
+ * (the P8 style fan-out reduction), placement priority survives the
3866
+ * merge unchanged.
3867
+ * 2. Label LAYER ORDER — MapLibre places symbols topmost-layer-first, so
3868
+ * `style-generator.ts` emits feature-label layers lowest-priority
3869
+ * first: safety-critical classes end up topmost and claim collision
3870
+ * space before anything else. (`symbol-sort-key` alone cannot do
3871
+ * this — it only orders placement WITHIN a layer.)
3815
3872
  *
3816
- * - `text-field` — the number itself, its unit conversion, its rounding.
3817
- * ENC content is never altered; a bigger "4" is still the cell's "4".
3818
- * - `text-color` / `text-halo-color` — §SOUNDG02's two shades against the
3819
- * safety contour, and the palette that draws them (red night included:
3820
- * it IS the NIGHT chart). A depth number shoaler than the contour stays
3821
- * the loud one.
3822
- * - `symbol-sort-key` — shallowest wins a collision, at any size.
3823
- * - `filter`, `minzoom`, `maxzoom`, layer ids and layer order — so the
3824
- * resident layer set the desktop builds at boot is the same set, and a
3825
- * switch is `setLayoutProperty` / `setPaintProperty` on layers already
3826
- * there. No layer and no draw call is added; what costs more is GPU fill,
3827
- * because a bigger glyph covers more pixels (about 6–7 % more blended
3828
- * fragments per pixel on the chart bench — the PR has the numbers).
3873
+ * The ladder (safety-critical > nav-aids > depth field > structures >
3874
+ * ambient notes):
3829
3875
  *
3830
- * ===========================================================================
3831
- * WHICH LABELS ARE "DEPTH NUMBERS"
3832
- * ===========================================================================
3876
+ * 100 — Safety-critical: light characteristics (LIGHTS), hazard depth
3877
+ * callouts (UWTROC/OBSTRN/WRECKS VALSOU), overhead clearances
3878
+ * (CBLOHD/PIPOHD VERCLR). Irreducible mariner-safety text.
3879
+ * 200 — Nav-aid designations: buoy / beacon `R "4"` labels.
3880
+ * 250 — Auxiliary nav-aid info: fog signal type (FOGSIG).
3881
+ * 300 — Depth field: soundings, contour values. Individually droppable —
3882
+ * a hidden sounding has visible neighbors.
3883
+ * 400 — Structures: bridges, landmarks, piers, moorings, buildings.
3884
+ * 500 — Ambient annotation: seabed nature, magnetic variation, kelp.
3885
+ * 900 — Orientation names (SEAARE etc.) — not part of this ladder;
3886
+ * they keep their settled watermark model (cell-band fan +
3887
+ * dominance composition + `orientationLabelLayout`) and always
3888
+ * paint underneath feature labels.
3833
3889
  *
3834
- * Exactly the ones the depth-unit switch converts (`buildLabelLayer` in
3835
- * `layer-builder.ts`): soundings, contour values, and the depth printed beside
3836
- * a rock, wreck or obstruction. The unit switch already treats those three as
3837
- * one claim — its own note says a rock's figure "is the SAME depth claim as a
3838
- * sounding" — and a size switch that enlarged the soundings around a rock but
3839
- * left the rock's own depth small would make the hazard the quietest number
3840
- * on that part of the chart.
3890
+ * Safety guardrail: a lower rung never blocks a higher rung — placement
3891
+ * order guarantees the safety-critical label got the space first. Density
3892
+ * relief comes from dropping the LOWEST rung in a pile-up, never the top.
3893
+ */
3894
+ declare const LABEL_PRIORITY: Record<string, number>;
3895
+ /**
3896
+ * Resolve the priority rank for an S-57 class's label. Orientation classes
3897
+ * report 900 regardless of ladder membership (PRDARE etc.) so the
3898
+ * layer-order sort in style-generator.ts can treat priority uniformly.
3899
+ */
3900
+ declare function getLabelPriority(layerName: string): number;
3901
+ /**
3902
+ * Compose a `symbol-sort-key` from a class rank and an optional
3903
+ * within-class key expression. The within-class key is clamped to
3904
+ * [0, 999] so it can never bleed into a neighboring rank's band.
3905
+ */
3906
+ declare function labelSortKey(rank: number, within?: unknown): ExpressionSpecification | number;
3907
+ /**
3908
+ * Extract the trailing designation token from a nav-aid OBJNAM.
3841
3909
  *
3842
- * Not the dredged-channel note (`DRGNOTE`): it is a sentence fitted inside
3843
- * its own channel polygon at a computed size, and scaling it would stop it
3844
- * fitting. Not the bridge note: a clearance is a height, not a depth.
3910
+ * NOAA names channel aids "<waterway> <type> Buoy|Beacon <designation>"
3911
+ * where the designation is 1-4 alphanumerics: "4", "18A", "B", "BG".
3912
+ * Returns null when the name has no designation (trailing token is a
3913
+ * stopword type-name, or longer than 4 chars).
3845
3914
  *
3846
- * ===========================================================================
3847
- * WHY NOT A BOLD FACE
3848
- * ===========================================================================
3915
+ * This is the TS twin of `navAidDesignationTextExpression` — the GeoJSON
3916
+ * preprocessing path uses this function, the PMTiles path uses the GL
3917
+ * expression. Keep the two in agreement (see label-priority.test.ts).
3918
+ */
3919
+ declare function extractNavAidDesignation(objnam: string): string | null;
3920
+ /** Get label config for a given S-57 layer, or null if no labels */
3921
+ declare function getLabelConfig(layerName: string, opts?: LabelConfigOptions): LabelLayerConfig | null;
3922
+ /**
3923
+ * Build a Mapbox symbol layer spec from a label config.
3924
+ */
3925
+ declare function buildLabelLayer(layerId: string, sourceId: string, config: LabelLayerConfig, visible: boolean, overrides?: {
3926
+ textScale?: number;
3927
+ minzoom?: number | null;
3928
+ allowOverlap?: boolean;
3929
+ }): maplibregl.LayerSpecification;
3930
+ /**
3931
+ * Pre-process SOUNDG GeoJSON to extract Z coordinates as `depth` properties.
3932
+ * Soundings are MultiPoint with Z values in geometry.coordinates[][2].
3933
+ * This explodes MultiPoints into individual Points with a `depth` property.
3934
+ */
3935
+ declare function preprocessSoundings(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3936
+ /**
3937
+ * Pre-process LIGHTS GeoJSON to derive compact light characteristic labels.
3938
+ * Adds `_lightLabel` property (e.g., "Fl(2) WR 4s 5M").
3849
3939
  *
3850
- * The chart's glyph sets are Open Sans Regular, Semibold and Italic — the tile
3851
- * worker's `/v1/glyphs` (`cloudflare/tile-worker/glyphs/GLYPHS.json`) and the
3852
- * offline chart-assets pack serve those three and nothing else, and a
3853
- * fontstack no glyph host serves is the failure `labels.ts`'s SOUNDG note
3854
- * describes (Native lost every sounding). Soundings are already Semibold. So "bolder" here is
3855
- * size: stroke width scales with the glyph, and at 1x device pixels (a Pi's
3856
- * panel) a 14 px Semibold numeral is both heavier and crisper than a 10 px one.
3940
+ * KEEP IN STEP WITH `buildLightLabelExpression()` in layer-builder.ts — that
3941
+ * is the same label built as a GL expression for the tile path, which has no
3942
+ * JS preprocessing step. `light-label-parity.test.ts` asserts the two agree
3943
+ * on real tile features; if you change one, change both.
3944
+ */
3945
+ declare function preprocessLights(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3946
+ /** List of layers that have labels */
3947
+ declare const LABELED_LAYERS: Set<string>;
3948
+ /**
3949
+ * Pre-process buoy/beacon GeoJSON to derive concise mariner-style labels.
3950
+ * Adds `_navAidLabel` property (e.g., 'R "8"', 'G "5"').
3951
+ */
3952
+ declare function preprocessNavAids(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3953
+ /** Check if a layer name needs nav aid preprocessing */
3954
+ declare function isNavAidLayer(layerName: string): boolean;
3955
+ /**
3956
+ * Pre-process water body polygon GeoJSON into centroid points for label placement.
3957
+ * Computes a simple average-coordinate centroid for each polygon feature that has OBJNAM.
3958
+ */
3959
+ declare function preprocessWaterBodies(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3960
+ /** MVT layer the build-side place-name index emits. */
3961
+ declare const PLACENAME_SOURCE_LAYER = "PLACENAME";
3962
+ /** Orientation classes whose names render italic (water voice). */
3963
+ declare const PLACENAME_WATER_CLASSES: readonly string[];
3964
+ /** Orientation classes whose names render upright (land voice). */
3965
+ declare const PLACENAME_LAND_CLASSES: readonly string[];
3966
+ /**
3967
+ * The 2 PLACENAME symbol layers. Layout contract (design §2.2): collision
3968
+ * ON (`allow-overlap: false`, `ignore-placement: false`,
3969
+ * `optional: true`), padding 4, `text-variable-anchor`, and
3970
+ * `symbol-sort-key` straight from the build-side `rank` — MapLibre places
3971
+ * LOWER sort keys first, and rank is minted lower-is-more-important
3972
+ * (tier major, bbox area minor), so when space runs out the biggest names
3973
+ * survive. That ranking is the per-zoom density governor P4 asked for.
3857
3974
  */
3975
+ declare function buildPlaceNameLayers(sourceId: string, ct?: ColorTableName): maplibregl.LayerSpecification[];
3858
3976
 
3859
- /** The two choices, in presentation order. */
3860
- declare const DEPTH_LABEL_SIZES: readonly ["standard", "large"];
3861
- type DepthLabelSize = (typeof DEPTH_LABEL_SIZES)[number];
3862
3977
  /**
3863
- * Standard is the chart as it has always been drawn, and it is what a style
3864
- * generated without the option gets: byte-identical, so the served style, the
3865
- * native-render goldens and every client that never asks are unchanged.
3978
+ * Spatial grid-based feature decimation for performance at low zoom levels.
3979
+ * Thins dense GeoJSON feature sets by keeping only one representative
3980
+ * feature per grid cell per layer.
3866
3981
  */
3867
- declare const DEFAULT_DEPTH_LABEL_SIZE: DepthLabelSize;
3868
- /** Case-insensitive; anything unrecognised is `null`, never a throw. */
3869
- declare function parseDepthLabelSize(raw: string | null | undefined): DepthLabelSize | null;
3870
- /** The S-57 classes whose label is a depth figure. See the header. */
3871
- declare const DEPTH_LABEL_CLASSES: ReadonlySet<string>;
3982
+ interface DecimationOptions {
3983
+ /** Grid resolution (cells per axis). Default 80. */
3984
+ gridSize?: number;
3985
+ /** Zoom level below which decimation kicks in. Default 12. */
3986
+ zoomThreshold?: number;
3987
+ }
3872
3988
  /**
3873
- * Sounding collision padding at Large, px. Standard is 6.
3874
- *
3875
- * ⚠ THIS IS THE ONE THAT DECIDES HOW MANY SOUNDINGS SURVIVE. MapLibre drops a
3876
- * sounding whose box (glyphs + padding) overlaps one already placed — the
3877
- * shallowest places first, so what drops is the deeper figure. A 1.4x numeral
3878
- * inside Standard's 6 px padding lost a quarter of the soundings: bigger
3879
- * numbers, fewer of them, which is not what was asked for. Trimming the
3880
- * padding gives the box back.
3881
- *
3882
- * Measured 2026-10-05 on the chart bench (`scripts/chart-bench`, Boston, DAY,
3883
- * 1280x800 at 1x), soundings placed, Standard → Large at this padding:
3884
- *
3885
- * z11.5 515 → 539 (+5%) z12.3 434 → 415 (−4%)
3886
- * z13 374 → 366 (−2%) z14 326 → 311 (−5%)
3887
- *
3888
- * and at Standard's 6 px: 391 / 246 at z11.5 / z14 (−24% / −25%). 3 px kept
3889
- * z14 level but put 17% MORE figures on the coastal view, which reads as
3890
- * clutter rather than as the same chart drawn bigger.
3989
+ * Zoom-adaptive grid resolution for decimation.
3891
3990
  */
3892
- declare const LARGE_SOUNDING_PADDING_PX = 3.5;
3991
+ declare function getGridSizeForZoom(zoom: number): number;
3893
3992
  /**
3894
- * Apply a depth-label size to one generated label layer.
3993
+ * Decimate a GeoJSON FeatureCollection using spatial grid thinning.
3994
+ * Only runs when zoom < zoomThreshold. Returns the original FC otherwise.
3895
3995
  *
3896
- * Standard — and any class that is not a depth figure — returns the SAME
3897
- * object, untouched. Large returns a copy with only the properties listed in
3898
- * the header changed.
3996
+ * @param fc - Input FeatureCollection
3997
+ * @param layerName - S-57 layer name (affects priority logic)
3998
+ * @param currentZoom - Current map zoom level
3999
+ * @param bounds - Current viewport bounds
4000
+ * @param opts - Decimation options
3899
4001
  */
3900
- declare function applyDepthLabelSize(objectClass: string, layer: StyleLayer, size: DepthLabelSize | undefined): StyleLayer;
3901
-
4002
+ declare function decimateFeatures(fc: GeoJSON.FeatureCollection, layerName: string, currentZoom: number, bounds: {
4003
+ west: number;
4004
+ south: number;
4005
+ east: number;
4006
+ north: number;
4007
+ }, opts?: DecimationOptions): GeoJSON.FeatureCollection;
3902
4008
  /**
3903
- * Builds Mapbox GL Style layer specs from S-52 lookup entries.
3904
- * Wraps the existing atlas resolveStyle() and adds source/source-layer/filter.
4009
+ * Get the max number of cells to load based on zoom level.
3905
4010
  */
4011
+ declare function getMaxCellsForZoom(zoom: number): number;
3906
4012
 
3907
- interface StyleLayer {
3908
- id: string;
3909
- type: string;
3910
- source: string;
3911
- "source-layer": string;
3912
- filter?: unknown[];
3913
- paint: Record<string, unknown>;
3914
- layout?: Record<string, unknown>;
3915
- minzoom?: number;
3916
- maxzoom?: number;
3917
- metadata?: Record<string, unknown>;
3918
- }
3919
4013
  /**
3920
- * Build all Mapbox GL layers for a given S-57 object class.
3921
- * Returns multiple layers (fill, outline, symbol, label, etc.).
4014
+ * IHO S-52 / NOAA Chart No. 1 / INT1 nautical symbol SVGs.
3922
4015
  *
3923
- * Post-consolidation: all S-57 classes live as MVT layers inside one
3924
- * consolidated PMTiles per region; `source` is always the region name.
4016
+ * Complete symbology suite following:
4017
+ * - IHO S-52 Presentation Library (Ed 4.0)
4018
+ * - IALA Maritime Buoyage System R1001 (Region B for US waters)
4019
+ * - NOAA Chart No. 1 (Ed 13) / INT1
3925
4020
  *
3926
- * `opts.safetyContour` is the mariner's safety depth in metres — it drives
3927
- * the §CSYM02 isolated-danger ring on hazard classes. Defaults to
3928
- * `DEFAULT_SAFETY_CONTOUR_M`, the same default the SOUNDG two-shade rule
3929
- * uses, so the ring and the sounding colour always agree.
4021
+ * Buoy shape terminology (US / Chart No. 1):
4022
+ * - CAN (Q20): Flat-top cylindrical buoy — Port mark in IALA-B (red)
4023
+ * - NUN (Q21): Conical/pointed-top buoy — Starboard mark in IALA-B (green)
4024
+ * - PILLAR: Tall narrow buoy — used for cardinal, isolated danger, etc.
4025
+ * - SPAR: Tall thin pole buoy
4026
+ * - SPHERICAL: Round buoy — safe water marks
4027
+ * - BARREL: Barrel/tun shaped buoy
4028
+ * - SUPER: Large navigation buoy / LANBY
4029
+ *
4030
+ * Cardinal mark topmark orientations (IALA):
4031
+ * - North: both cones point UP ▲▲
4032
+ * - South: both cones point DOWN ▼▼
4033
+ * - East: cones base-to-base (diamond) ◆
4034
+ * - West: cones point-to-point (hourglass) ⊗
4035
+ *
4036
+ * Each symbol is a compact SVG at 32×32 for Mapbox GL `map.addImage()`.
3930
4037
  */
3931
- declare function buildLayersForClass(objectClass: string, ct: ColorTableName, sourceId?: string, opts?: {
3932
- safetyContour?: number;
3933
- }): StyleLayer[];
4038
+ /** Color used in the SVG with its S-52 token name, hex, and usage */
4039
+ interface SymbolColor {
4040
+ token: string;
4041
+ hex: string;
4042
+ usage: string;
4043
+ }
4044
+ /** Line style used in the SVG */
4045
+ interface SymbolLineStyle {
4046
+ element: string;
4047
+ width: number;
4048
+ pattern?: string;
4049
+ color: string;
4050
+ }
4051
+ /** Fill element used in the SVG */
4052
+ interface SymbolFillElement {
4053
+ element: string;
4054
+ color: string;
4055
+ opacity?: number;
4056
+ }
4057
+ interface NauticalSymbol {
4058
+ id: string;
4059
+ label: string;
4060
+ /** INT1 / Chart No. 1 reference */
4061
+ int1Ref: string;
4062
+ /** Chart No. 1 section letter */
4063
+ section: string;
4064
+ getSvg: () => string;
4065
+ /** Generate SVG with theme-specific colors (for bridge icons etc.) */
4066
+ getThemedSvg?: (chblk: string, movable: string) => string;
4067
+ /** What this symbol represents on a chart */
4068
+ description?: string;
4069
+ /** S-52 Presentation Library symbol name */
4070
+ s52PlRef?: string;
4071
+ /** S-57 object class (e.g. "LNDMRK") when it can't be derived from s52PlRef's prefix */
4072
+ s57Class?: string;
4073
+ /** Grouping key for family clustering */
4074
+ group?: string;
4075
+ /** IDs of related variant symbols */
4076
+ variants?: string[];
4077
+ /** S-57 attribute that selects this variant */
4078
+ attributeKey?: string;
4079
+ /** Value of that attribute for this variant */
4080
+ attributeValue?: number;
4081
+ /** Example label strings seen on charts */
4082
+ labelExamples?: string[];
4083
+ /** What the color represents */
4084
+ colorMeaning?: string;
4085
+ /** Colors used in the SVG */
4086
+ colors?: SymbolColor[];
4087
+ /** Line/stroke styles used in the SVG */
4088
+ lineStyles?: SymbolLineStyle[];
4089
+ /** Fill regions in the SVG */
4090
+ fillElements?: SymbolFillElement[];
4091
+ /** Expanded navigational usage context (2-4 sentences) */
4092
+ usageContext?: string;
4093
+ /** Render size for documentation */
4094
+ renderSize?: string;
4095
+ }
4096
+ /** A logical grouping of related symbols */
4097
+ interface SymbolGroup {
4098
+ id: string;
4099
+ label: string;
4100
+ section: string;
4101
+ description: string;
4102
+ symbolIds: string[];
4103
+ }
4104
+ /** Chart No. 1 section metadata */
4105
+ interface ChartSection {
4106
+ id: string;
4107
+ label: string;
4108
+ description: string;
4109
+ }
4110
+ declare const NAUTICAL_SYMBOLS: NauticalSymbol[];
4111
+ /** S-57 object class → default symbol ID mapping.
4112
+ * Values are S-52 PL symbol names (matching SVG filenames in s52/point/).
4113
+ * Conditional expressions in getIconExpression() override these for classes
4114
+ * with attribute-dependent variants.
4115
+ */
4116
+ declare const SYMBOL_MAP: Record<string, string>;
4117
+ /** Get the Mapbox icon-image expression for data-driven symbol selection.
4118
+ * Returns S-52 PL symbol names that correspond to SVG files in s52/point/.
4119
+ */
4120
+ declare function getIconExpression(layerName: string): string | unknown[];
3934
4121
  /**
3935
- * Get the full ordered list of object classes for style generation.
3936
- * Uses S52_DRAW_ORDER as base, then adds any missing classes from the
3937
- * S-52 lookup table so a newly-ingested class automatically gets styled
3938
- * without touching this file.
4122
+ * Browser-only image loaders (`loadSymbolImages`, `loadThemedBridgeImages`)
4123
+ * live in `./symbols-browser.ts` (sub-export `@chrtco/sdk/atlas/browser`) —
4124
+ * they require a DOM (use `new Image()` + `map.addImage()`).
3939
4125
  */
3940
- declare function getOrderedClasses(): string[];
4126
+ /** Get inline SVG string for a symbol by ID (for the Atlas legend) */
4127
+ declare function getSymbolSvgString(symbolId: string): string | null;
4128
+ declare const CHART_SECTIONS: ChartSection[];
4129
+ declare const SYMBOL_GROUPS: SymbolGroup[];
4130
+ /** Generate a single SVG sprite sheet containing all symbols as <symbol> elements */
4131
+ declare function generateSpriteSheet(): string;
4132
+ /** Generate a JSON manifest mapping symbol IDs to metadata */
4133
+ declare function generateSpriteManifest(): Record<string, {
4134
+ label: string;
4135
+ int1Ref: string;
4136
+ section: string;
4137
+ group?: string;
4138
+ s52PlRef?: string;
4139
+ description?: string;
4140
+ attributeKey?: string;
4141
+ attributeValue?: number;
4142
+ }>;
3941
4143
 
3942
4144
  /**
3943
- * Dominance fan-out collapse (P8 Phase 3.3 / D4 "axis 1").
3944
- *
3945
- * `makeBandVariants()` historically emitted, per (class, band): one
3946
- * "not-dominated" layer plus up to five `-domBy{D}` copies that differ ONLY
3947
- * in their dominance filter and a `maxzoom` of `dominatedVariantMaxzoom(D)`.
3948
- * That fan existed because MapLibre filters could not read `["zoom"]`. They
3949
- * can now (style-spec 24.8.5; buckets evaluate at the tile's `overscaledZ`),
3950
- * so the whole fan folds into ONE layer per (class, band) whose filter
3951
- * carries the zoom gate.
3952
- *
3953
- * **Construction principle — read before changing anything here.** The
3954
- * collapsed filter is the literal `["any"]` UNION of the clauses the fan
3955
- * already encoded: every variant the legacy emitter would have produced
3956
- * contributes exactly one `["all", <its dominance filter>, ["<", ["zoom"],
3957
- * <its maxzoom>]]` clause, and the dead-variant skip is mirrored too.
3958
- * Equivalence is therefore *structural*, not re-derived — which matters,
3959
- * because the two "obvious" hand-written forms are both wrong:
3960
- *
3961
- * 1. A `["match", ["get","_dominated_by_min"], 2,5, 3,7, 4,9, 5,11, 6,11, 24]`
3962
- * threshold (the form the design doc proposed) uses band 6's *eligibility*
3963
- * zoom 11. The real ceiling is `dominatedVariantMaxzoom("6") = 13` — band
3964
- * 6's DATA floor. Using 11 reopens the z11–z12 void window that
3965
- * `band-priority.ts` documents closing.
3966
- * 2. That same form silently drops the legacy `_dominated: true` boolean,
3967
- * which `dominatedByMinFilter("6")` still honours. A legacy-tagged feature
3968
- * has no `_dominated_by_min`, so `match` falls to its default and the
3969
- * feature renders FOREVER instead of dropping at z13.
3970
- *
3971
- * Deriving the gate from the emitter's own clauses avoids both by construction.
3972
- * `band-collapse.test.ts` re-proves it on every CI run by differentially
3973
- * evaluating legacy-vs-collapsed through MapLibre's real three gates.
4145
+ * IHO S-101 ENC Product Specification — Feature Catalogue
3974
4146
  *
3975
- * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
3976
- * axis 1 ONLY: the per-band layers survive, with identical ids and identical
3977
- * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
3978
- * the per-band copies into one layer per class) was evaluated and **KILLED**
3979
- * for feature layers — it is unsound for the opaque-fill classes
3980
- * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
3981
- * order is the ONLY thing separating the bands, and 15/15 class pairs are
3982
- * provably unsatisfiable with one layer each. Do not reintroduce it there.
3983
- * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
4147
+ * S-101 replaces S-57 as the data model for Electronic Navigational Charts.
4148
+ * This catalogue maps S-101 feature classes to:
4149
+ * - S-57 equivalents (where they exist)
4150
+ * - Chart No. 1 sections (same organizational structure)
4151
+ * - Portrayal catalogue references (S-100 Portrayal Model)
3984
4152
  *
3985
- * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
3986
- * collapses its whole render pass above 482 symbol layers, and the served
3987
- * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
3988
- * fan into ONE layer per class, and `collapsedOrientationGate` folds the
3989
- * orientation fan's per-dominator variants into its per-cell layers. That is
3990
- * NOT the killed axis 2, because what the fill argument protects — paint
3991
- * order as the sole band separator — does not apply to labels: their
3992
- * load-bearing order is the cross-class P4 priority ladder (each class keeps
3993
- * exactly one layer in its ladder slot), and within-class placement priority
3994
- * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
3995
- * documents as designed to survive precisely this merge.
4153
+ * Key S-101 changes from S-57:
4154
+ * - CamelCase feature class names (DepthArea vs DEPARE)
4155
+ * - Complex/structured attributes (nested objects vs flat)
4156
+ * - New feature types for AIS, quality, under-keel clearance
4157
+ * - Removed/merged features for simplification
3996
4158
  *
3997
- * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
3998
- * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
3999
- * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
4000
- * data floor, on the live public Style API path. Renaming the id silently
4001
- * unclamps those overrides. Cross-band LABEL layers are the deliberate
4002
- * exception: they carry every band floor INSIDE the filter (tile-zoom
4003
- * clauses a customer override cannot touch), which supersedes the clamp —
4004
- * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
4005
- * Both halves asserted in `band-collapse.test.ts`.
4006
- */
4007
-
4008
- /**
4009
- * Which emitter the style generator uses for the dominance fan.
4010
- * `"legacy"` exists so CI can differentially prove the two equivalent (it is
4011
- * the oracle in `band-collapse.test.ts`) and as a one-line rollback. It is an
4012
- * internal knob — deliberately NOT on the exported `StyleGeneratorOptions`,
4013
- * so it cannot reach `@chrtco/sdk` consumers via `ChrtStyleOptions`.
4159
+ * Reference: IHO S-101 Product Specification (Ed 1.1.0)
4160
+ * IHO S-100 Universal Hydrographic Data Model
4014
4161
  */
4015
- type BandFanMode = "collapsed" | "legacy";
4162
+ interface S101Feature {
4163
+ /** S-101 camelCase feature class name */
4164
+ featureClass: string;
4165
+ /** Human-readable name */
4166
+ name: string;
4167
+ /** S-57 6-char equivalent acronym, if any */
4168
+ s57Equivalent?: string;
4169
+ /** Chart No. 1 section letter */
4170
+ section: string;
4171
+ /** Group key for clustering */
4172
+ group: string;
4173
+ /** Feature category */
4174
+ category: "geo" | "meta" | "info";
4175
+ /** Description of the feature */
4176
+ description: string;
4177
+ /** S-100 Portrayal Catalogue reference */
4178
+ portrayalRef?: string;
4179
+ /** True if this feature has no S-57 equivalent */
4180
+ isNewInS101: boolean;
4181
+ /** Key attributes specific to this feature */
4182
+ attributes?: string[];
4183
+ }
4184
+ declare const S101_FEATURES: S101Feature[];
4185
+ declare const S101_CHART_SECTIONS: {
4186
+ id: string;
4187
+ label: string;
4188
+ description: string;
4189
+ }[];
4190
+ declare const S101_SYMBOL_GROUPS: {
4191
+ id: string;
4192
+ label: string;
4193
+ section: string;
4194
+ description: string;
4195
+ }[];
4016
4196
 
4017
4197
  /**
4018
- * Generates a complete Mapbox GL Style Specification v8 JSON
4019
- * from the S-52 atlas definitions.
4198
+ * S-101 Portrayal Catalogue — New Symbol SVGs
4199
+ *
4200
+ * These symbols represent features introduced in S-101 that have no
4201
+ * direct S-57/S-52 equivalent. The existing S-52 symbols are reused
4202
+ * where S-101 retains visual continuity with the S-57 portrayal.
4203
+ *
4204
+ * Reference: IHO S-101 Portrayal Catalogue (Ed 1.1.0)
4020
4205
  */
4021
4206
 
4022
- interface MapboxStyle {
4023
- version: 8;
4024
- name: string;
4025
- metadata: Record<string, unknown>;
4026
- sources: Record<string, unknown>;
4027
- sprite?: string;
4028
- glyphs: string;
4029
- layers: StyleLayer[];
4030
- }
4031
- interface StyleGeneratorOptions extends BuildSourcesOptions {
4032
- /**
4033
- * When set, emit a "paper chart" style centered on the selected scale
4034
- * band, with graceful coverage fallback: the requested band's layers
4035
- * draw on top where its cells have data; coarser bands (driven by
4036
- * `CHART_CASCADE_CODES`) fill gaps where the requested band has no
4037
- * cells. Per-feature SCAMIN minzooms are preserved on every cascade
4038
- * variant; `maxzoom` is dropped so the user can zoom in indefinitely.
4039
- *
4040
- * When omitted (default), continuous mode emits one layer copy per
4041
- * source band with strict per-band zoom windows so exactly one band
4042
- * renders at any given continuous zoom.
4043
- *
4044
- * `lockedBounds` (optional) restricts chart-mode rendering to a
4045
- * specific bbox via a `["within"]` filter — applied ONLY to the
4046
- * requested band's variants. Cascade fallback bands render across the
4047
- * full viewport so a chart-Harbor user panning offshore still sees
4048
- * Approach/Coastal data. The frontend pairs this with a translucent
4049
- * grey overlay outside the bbox so the lock boundary is still visible
4050
- * to the user (the fallback data is dimmed by the overlay but not
4051
- * filtered out).
4052
- */
4053
- chartMode?: {
4054
- band: ScaleBand$1;
4055
- lockedBounds?: BBox;
4056
- };
4057
- /**
4058
- * Display unit for depth labels (SOUNDG soundings, DEPCNT contour
4059
- * values). Source data is always meters (S-57 / VALSOU / VALDCO);
4060
- * "feet" wraps the text-field expression with a unit conversion +
4061
- * round so the rendered numbers match. Default "meters".
4062
- */
4063
- depthUnits?: "meters" | "feet";
4064
- /**
4065
- * Which symbol artwork the style references. "s52" (default) uses the
4066
- * PresLib-derived S-52 set; "s101" rewrites icon references to the
4067
- * official S-101 Portrayal Catalogue symbols (s101: image namespace)
4068
- * where a counterpart exists — see s101-style.ts. "chrt" applies the
4069
- * CHRT house ATON/LIGHTS glyphs with colour-true tinting — see
4070
- * chrt-style.ts. Selection logic differences are documented per module.
4071
- */
4072
- symbolSet?: "s52" | "s101" | "chrt" | "raster";
4073
- /**
4074
- * The mariner's safety contour in metres — the depth below which a
4075
- * feature is "dangerous to my hull". Drives both safety-contour
4076
- * conditional symbology rules:
4077
- *
4078
- * - §SOUNDG02 — soundings shoaler than this render in the
4079
- * high-contrast color; deeper soundings render in the muted color.
4080
- * - §CSYM02 — isolated dangers (rocks / wrecks / obstructions)
4081
- * shoaler than this get the DNGCIR01 danger ring.
4082
- *
4083
- * Default `DEFAULT_SAFETY_CONTOUR_M` (10 m).
4084
- *
4085
- * Does not affect the DEPARE depth-area shading, whose 2/5/10 m bands
4086
- * are a separate S-52 concept.
4087
- */
4088
- safetyContour?: number;
4089
- /**
4090
- * When false, all soundings render in a single shade. Default true.
4091
- */
4092
- twoShadeSoundings?: boolean;
4093
- /**
4094
- * Label model for the 9 orientation place-name classes (SEAARE, LNDRGN,
4095
- * LAKARE, RIVERS, CANALS, BUAARE, AIRARE, LNDARE, PRDARE).
4096
- *
4097
- * "legacy" (default) — the per-cell-band orientation fan
4098
- * (fanOrientationLabelByCellBand): one label per feature per band,
4099
- * deconflicted only by `_label_anchor` tags and watermark opacity.
4100
- *
4101
- * "placename" — 2 PLACENAME symbol layers over the build-side
4102
- * place-name index (CHRTbaremetal placeNameIndex.ts): one collision-
4103
- * managed label per named place with a per-feature zoom window; the
4104
- * orientation fan for those classes is NOT emitted. Requires tiles
4105
- * built with the PLACENAME side index — on older tiles the layers
4106
- * simply draw nothing (soft-fail is index-side), which is why the
4107
- * default stays "legacy" until L2c's rebuild + Maine-coast visual
4108
- * verification flips it (docs/design/chart-label-deconfliction.md
4109
- * §2.3). Feature-label classes are untouched either way.
4110
- */
4111
- labels?: "legacy" | "placename";
4112
- /**
4113
- * Font weight for SOUNDG labels. Default "semibold" — crisper numerals
4114
- * at small sizes on screen.
4115
- */
4116
- soundingFontWeight?: "regular" | "semibold";
4117
- /**
4118
- * How big the depth figures are drawn — soundings, contour values and the
4119
- * depth beside a rock, wreck or obstruction. "standard" (default) is
4120
- * byte-identical to omitting it; "large" enlarges those labels only, and
4121
- * never their text, colours, sort keys, filters or layer set. See
4122
- * depth-label-size.ts.
4123
- */
4124
- depthLabelSize?: DepthLabelSize;
4125
- }
4126
- declare function generateStyle(palette?: ColorTableName, opts?: StyleGeneratorOptions & {
4127
- bandFan?: BandFanMode;
4128
- }): MapboxStyle;
4129
- declare function generateStyleJSON(palette?: ColorTableName, opts?: StyleGeneratorOptions): string;
4130
- /** Get summary stats about the generated style */
4131
- declare function getStyleStats(style: MapboxStyle): {
4132
- totalLayers: number;
4133
- fillLayers: number;
4134
- lineLayers: number;
4135
- symbolLayers: number;
4136
- circleLayers: number;
4137
- sources: number;
4138
- objectClasses: Set<string>;
4139
- };
4207
+ declare const S101_NEW_SYMBOLS: NauticalSymbol[];
4140
4208
 
4141
4209
  declare const S101_SOURCE_COMMIT = "05063500e898544c90078ab78a61efc72b7e62f3";
4142
4210
 
@@ -4847,7 +4915,11 @@ declare const DEFAULT_CUSTOM_STYLE_CONFIG: CustomStyleConfig;
4847
4915
  * fields fall back to the platform defaults. Never throws.
4848
4916
  */
4849
4917
  declare function sanitizeCustomStyleConfig(raw: unknown): CustomStyleConfig;
4850
- /** Normalize a `?theme=` value to a colour table name; null for anything else. */
4918
+ /**
4919
+ * Normalize a `?theme=` value to a colour table name; null for anything else.
4920
+ * Case-insensitive; `-` is accepted for `_` (`eink-mono` = `eink_mono`), so
4921
+ * the e-ink names survive either spelling a caller guesses.
4922
+ */
4851
4923
  declare function normalizeTheme(raw: string | null | undefined): ColorTableName | null;
4852
4924
  interface ResolvedTheme {
4853
4925
  palette: ColorTableName;
@@ -5215,7 +5287,8 @@ declare class OfflineError extends Error {
5215
5287
 
5216
5288
  /** Options for `chrt.style()`. Extends the atlas style generator options. */
5217
5289
  interface ChrtStyleOptions extends StyleGeneratorOptions {
5218
- /** S-52 colour palette: "DAY" (default), "DUSK", or "NIGHT". */
5290
+ /** Colour palette: "DAY" (default), "DUSK", "NIGHT", or the e-paper
5291
+ * palettes "EINK_COLOR" / "EINK_MONO" / "EINK_NIGHT" (see `EINK_COLOR_TABLES`). */
5219
5292
  palette?: ColorTableName;
5220
5293
  }
5221
5294
  declare class Chrt {
@@ -5504,4 +5577,4 @@ declare class Chrt {
5504
5577
  warmTiles(region?: string): Promise<TileTokenResponse>;
5505
5578
  }
5506
5579
 
5507
- export { ALL_NAV_AID_LAYERS, ALL_WATER_BODY_LAYERS, AREA_QUERY_LAYERS, ATON_EXPRESSION_CLASSES, type AheadCoverageCell, type AheadFeatureProperties, type AheadOptions, type AheadResponse, type AlongRouteFeatureProperties, type AlongRouteOptions, type AlongRouteResponse, type AreaInspectResult, type AtlasStyle, type AttachOptions, BAND_CODE_ALIASES, BAND_DRAW_ORDER, BAND_PRIORITY_RANGES, BASE_ATTRIBUTION, BASE_BUNDLE_ATTRIBUTION, BASE_BUNDLE_ID, BASE_BUNDLE_MAXZOOM, BASE_BUNDLE_SCHEMA, BASE_BUNDLE_SOURCE_LAYERS, BASE_CLIENT_RASTER_BEFORE_ID, BASE_MAXZOOM, BASE_MINZOOM, BASE_REGION, type BBox, BRIDGE_QUERY_LAYERS, BUILD_MAXZOOM, BUILD_MINZOOM, BUILD_PARAM, type BandCode, type BandPriorityRange, type BridgeInspectResult, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_BAND_CODES, CHART_CASCADE_CODES, CHART_SECTIONS, CHRTRASTER_REF_CROPS, CHRTRASTER_SVGS, CHRT_ATON_COLORS, CHRT_GLYPH_NAMES, CHRT_GLYPH_SVGS, CHRT_ICON_EXPRESSIONS, CHRT_ICON_PREFIX, CHRT_SDF_GLYPHS, CHRT_STYLE_MODES, type CandidateQueryOptions, type CandidatesEventContext, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, type ChrtStyleMode, type ChrtStyleOptions, type ChrtStyleTheme, type ChrtSymbolSet, type ClickAnchor, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageRouteCell, type CoverageRouteOptions, type CoverageRouteResponse, type CoverageSource, type CustomStyleConfig, DEFAULT_CLICK_TOLERANCE, DEFAULT_CUSTOM_STYLE_CONFIG, DEFAULT_DEPTH_LABEL_SIZE, DEFAULT_INSPECT_CLASS_RANK, DEFAULT_LABEL_SETTINGS, DEFAULT_NAV_AID_SETTINGS, DEFAULT_REGION, DEFAULT_SAFETY_CONTOUR_M, DEFAULT_SOUNDING_SETTINGS, DEFAULT_TILE_BASE_URL, DEFAULT_TILE_REGION, DEFAULT_WATER_BODY_SETTINGS, DEPARE_FILL_BRANCH_TOKENS, DEPARE_FILL_FALLBACK_TOKEN, DEPTH_LABEL_CLASSES, DEPTH_LABEL_SIZES, DRAPEABLE_LAYER_TYPES, type DecodedAreaFeature, type DecodedBridge, type DecodedNavAid, type DecodedProperty, type DepthLabelSize, type DisplayCategory, type DisplayMode, type FeatureCollection, type FeaturesOptions, type FetchStyleOptions, FileSystemAdapter, type FlashPattern, type FlashPhase, type GenerateCustomStyleOptions, type GeneratedCustomStyle, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, INSPECT_CLASS_RANK, INSPECT_RANK_DEVIATIONS, type InspectCandidate, type InspectCandidateSet, type InspectEnrichment, type InspectEventContext, type InspectNotice, type InspectOptions, type InspectResult, type InspectTargetClass, type InspectableFeature, InteractClient, type InteractEntitlement, InteractError, type InteractErrorCode, type InteractHandle, type InteractMapLike, type InteractMapMouseEvent, LABELED_LAYERS, LABEL_BEHAVIOR, LABEL_LAYER_GROUPS, LABEL_PRIORITY, LAMP_CORE_LAYER_ID, LAMP_GLOW_LAYER_ID, LAMP_LAYER_IDS, LAMP_TICK_MS, LAND_ATTRIBUTION, LAND_BUILD_MAXZOOM, LAND_REGION, LARGE_SOUNDING_PADDING_PX, type LabelBehavior, type LabelConfigOptions, type LabelLayerConfig, type LabelSettings, type LampLayer, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, type MarineAlert, type MarineAlertsOptions, type MarineAlertsResponse, type MarineConditionsObservation, type MarineConditionsOptions, type MarineConditionsResponse, type MarineConditionsSeries, type MarineForecastOptions, type MarineForecastPeriod, type MarineForecastResponse, type MarineSeas, type MarineStation, type MarineSwellTrain, type MarineWind, type MarineZoneRef, NAUTICAL_SYMBOLS, NAV_AID_LAYER_GROUPS, NAV_AID_QUERY_LAYERS, NIGHT_INK, NIGHT_MUTED_INK, NON_SOLAS_DISCLAIMER, NON_SOLAS_DISCLAIMER_SHORT, type NauticalCategory, type NauticalSymbol, type NavAidInspectResult, type NavAidSettings, type NearbyMarineStation, OPAQUE_SPLIT_OF, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, PLACENAME_LAND_CLASSES, PLACENAME_SOURCE_LAYER, PLACENAME_WATER_CLASSES, type ParsedSigseqTerms, type PmtilesVectorSource, type Poi, type PoiCoverage, type PoiDataset, type PoiOptions, type PoiResponse, type QueryFeatureProperties, type QueryOptions, type QueryResponse, RASTER_ATON_CLASSES, RASTER_GLYPH_NAMES, RASTER_ICON_PREFIX, RELIEF_ATTRIBUTION, RELIEF_ENCODING, RELIEF_REGION, RELIEF_TILE_SIZE, type ResolvedStyle, type ResolvedTheme, type S101ArtworkProvenance, type S101Feature, S101_AREA_FILL_NAMES, S101_CHART_SECTIONS, S101_FEATURES, S101_ICON_PREFIX, S101_LINE_STYLE_NAMES, S101_NEW_SYMBOLS, S101_NO_OFFICIAL_COUNTERPART, S101_SOURCE_COMMIT, S101_SYMBOL_GROUPS, S101_SYMBOL_NAMES, S52_ATON_LOOKUPS, S52_ATON_LOOKUPS_PAPER, S52_DRAW_ORDER, S52_LOOKUP, S52_PATTERN_SPRITE_PREFIX, SIGPER_TOLERANCE_SECONDS, SIGSEQ_MAX_TERMS, SOUNDING_LAYERS, SYMBOL_GROUPS, SYMBOL_MAP, type ScaleBand, type SearchCoverageNotice, type SearchHit, type SearchOptions, type SearchResponse, type SectorArcFeature, type SectorArcFeatureCollection, type SectorInfo, type SigseqRefusal, type SigseqTerm, type SoundingSettings, type SourceMode, type StyleGeneratorOptions, type StyleLayer, type StyleUrlOptions, type SymbolGroup, TileAuth, type TileGroup, type TileGroupsResponse, TileTokenError, type TileTokenErrorCode, type TileTokenResponse, type TransformRequest, type TransformedRequest, type VectorSourceSpec, type VesselProfile, WATER_BODY_LAYER_GROUPS, type WaterBodySettings, WeatherEndpoint, type WeatherReading, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyBaseBundle, applyChrtSymbolSet, applyColorTheme, applyCustomStyleSettings, applyDepthLabelSize, applyRasterSymbolSet, applyS101SymbolSet, atonIconExpression, bandFilterExpression, buildAirportTierExpression, buildBuiltUpAreaTierExpression, buildFlashPattern, buildLabelLayer, buildLakeTierExpression, buildLampLayers, buildLandAreaTierExpression, buildLandRegionTierExpression, buildLayersForClass, buildOfflineStyle, buildPlaceNameLayers, buildRiverTierExpression, buildSeaAreaTierExpression, buildSectorArcGeoJSON, buildSectorArcsFC, buildSectorsFromFeatures, buildSources, candidateRank, capLuminance, cellBandCharacters, cellBandMatch, chrtAtonTintExpression, chrtHouseColor, chrtLightTintExpression, collectCandidates, collectClickCandidates, combineWithBandFilter, contrastRatio, daymarkColor, decimateFeatures, decodeAreaFeatureProps, decodeBridgeProps, decodeCandidate, decodeNavAidProps, depareColor, depareFillExpression, depthAreaClassLayerId, depthAreaSplitIds, deriveMarinerName, drapeHalves, drapedFirst, extractNavAidDesignation, featureIdentity, generateCustomStyle, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelBehavior, getLabelConfig, getLabelPriority, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getRasterSvg, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS101Svg, getThemedS52Svg, getTopmarkMeaning, hazardColor, hazardFillExpression, isDepthAreaClassLayerId, isDrapeable, isLabelLayerVisible, isMapLive, isNavAidLayer, isNavAidLayerVisible, isSoundingLayerVisible, isVisibleInMode, isWaterBodyLayerVisible, labelSortKey, lampRhythmColour, lampRhythmOpacity, lateralColor, lateralFillExpression, lightColor, lightFillExpression, lightsIconExpression, normalizeStyleMode, normalizeTheme, opaqueSplitBaseOf, paginate, parseDepthLabelSize, parseListCodes, parseSigseqTerms, parseWebhook, patternSpriteId, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, rasterAtonIconExpression, relativeLuminance, resolveCustomTheme, resolveRasterImage, resolveS101Color, resolveStyle, resolveTargetClass, rhythmProperties, s101ArtworkProvenance, sanitizeCustomStyleConfig, scaleLinearLight, scaleSizeValue, scaminToMinZoom, sectorLimitToArcBearing, splitOpaqueDepthAreas, styleModeBand, terrainStackCount, themeChrtGlyph, tileTemplate, uncoveredSectorArcs, withinBboxFilter, worldMinusBboxPolygon };
5580
+ export { ALL_NAV_AID_LAYERS, ALL_WATER_BODY_LAYERS, AREA_QUERY_LAYERS, ATON_EXPRESSION_CLASSES, type AheadCoverageCell, type AheadFeatureProperties, type AheadOptions, type AheadResponse, type AlongRouteFeatureProperties, type AlongRouteOptions, type AlongRouteResponse, type AreaInspectResult, type AtlasStyle, type AttachOptions, BAND_CODE_ALIASES, BAND_DRAW_ORDER, BAND_PRIORITY_RANGES, BASE_ATTRIBUTION, BASE_BUNDLE_ATTRIBUTION, BASE_BUNDLE_ID, BASE_BUNDLE_MAXZOOM, BASE_BUNDLE_SCHEMA, BASE_BUNDLE_SOURCE_LAYERS, BASE_CLIENT_RASTER_BEFORE_ID, BASE_MAXZOOM, BASE_MINZOOM, BASE_REGION, type BBox, BRIDGE_QUERY_LAYERS, BUILD_MAXZOOM, BUILD_MINZOOM, BUILD_PARAM, type BandCode, type BandPriorityRange, type BridgeInspectResult, type BuildOfflineStyleOptions, type BuildSourcesOptions, CHART_BAND_CODES, CHART_CASCADE_CODES, CHART_SECTIONS, CHRTRASTER_REF_CROPS, CHRTRASTER_SVGS, CHRT_ATON_COLORS, CHRT_GLYPH_NAMES, CHRT_GLYPH_SVGS, CHRT_ICON_EXPRESSIONS, CHRT_ICON_PREFIX, CHRT_SDF_GLYPHS, CHRT_STYLE_MODES, type CandidateQueryOptions, type CandidatesEventContext, type CellDeletedEvent, type CellDetail, type CellEventData, type CellNewEvent, type CellUpdatedData, type CellUpdatedEvent, type CellsOptions, type CellsResponse, type ChartSection, Chrt, type ChrtConfig, ChrtError, type ChrtStyleMode, type ChrtStyleOptions, type ChrtStyleTheme, type ChrtSymbolSet, type ClickAnchor, ColorTableName, type CoverageOptions, type CoverageResponse, type CoverageRouteCell, type CoverageRouteOptions, type CoverageRouteResponse, type CoverageSource, type CustomStyleConfig, DEFAULT_CLICK_TOLERANCE, DEFAULT_CUSTOM_STYLE_CONFIG, DEFAULT_DEPTH_LABEL_SIZE, DEFAULT_INSPECT_CLASS_RANK, DEFAULT_LABEL_SETTINGS, DEFAULT_NAV_AID_SETTINGS, DEFAULT_REGION, DEFAULT_SAFETY_CONTOUR_M, DEFAULT_SOUNDING_SETTINGS, DEFAULT_TILE_BASE_URL, DEFAULT_TILE_REGION, DEFAULT_WATER_BODY_SETTINGS, DEPARE_FILL_BRANCH_TOKENS, DEPARE_FILL_FALLBACK_TOKEN, DEPTH_LABEL_CLASSES, DEPTH_LABEL_SIZES, DRAPEABLE_LAYER_TYPES, type DecodedAreaFeature, type DecodedBridge, type DecodedNavAid, type DecodedProperty, type DepthLabelSize, type DisplayCategory, type DisplayMode, EINK_DANGER_PATTERN, EINK_DOTS_PATTERN, EINK_HATCH_PATTERN, type FeatureCollection, type FeaturesOptions, type FetchStyleOptions, FileSystemAdapter, type FlashPattern, type FlashPhase, type GenerateCustomStyleOptions, type GeneratedCustomStyle, type GeoJsonFeature, type GeoJsonGeometry, type GeomType, INSPECT_CLASS_RANK, INSPECT_RANK_DEVIATIONS, type InspectCandidate, type InspectCandidateSet, type InspectEnrichment, type InspectEventContext, type InspectNotice, type InspectOptions, type InspectResult, type InspectTargetClass, type InspectableFeature, InteractClient, type InteractEntitlement, InteractError, type InteractErrorCode, type InteractHandle, type InteractMapLike, type InteractMapMouseEvent, LABELED_LAYERS, LABEL_BEHAVIOR, LABEL_LAYER_GROUPS, LABEL_PRIORITY, LAMP_CORE_LAYER_ID, LAMP_GLOW_LAYER_ID, LAMP_LAYER_IDS, LAMP_TICK_MS, LAND_ATTRIBUTION, LAND_BUILD_MAXZOOM, LAND_REGION, LARGE_SOUNDING_PADDING_PX, type LabelBehavior, type LabelConfigOptions, type LabelLayerConfig, type LabelSettings, type LampLayer, type LookupEntry, type MapLibreStyle, type MapboxStyle, type MapboxVectorSource, type MarineAlert, type MarineAlertsOptions, type MarineAlertsResponse, type MarineConditionsObservation, type MarineConditionsOptions, type MarineConditionsResponse, type MarineConditionsSeries, type MarineForecastOptions, type MarineForecastPeriod, type MarineForecastResponse, type MarineSeas, type MarineStation, type MarineSwellTrain, type MarineWind, type MarineZoneRef, NAUTICAL_SYMBOLS, NAV_AID_LAYER_GROUPS, NAV_AID_QUERY_LAYERS, NIGHT_INK, NIGHT_MUTED_INK, NON_SOLAS_DISCLAIMER, NON_SOLAS_DISCLAIMER_SHORT, type NauticalCategory, type NauticalSymbol, type NavAidInspectResult, type NavAidSettings, type NearbyMarineStation, OPAQUE_SPLIT_OF, type OfflineBundleResponse, type OfflineDownloadOptions, type OfflineDownloadProgress, type OfflineDownloadResult, OfflineError, type OfflineErrorCode, type OfflineFetchStyleOptions, type OfflineHeartbeatOptions, type OfflineRegionSummary, type OfflineRemoveOptions, OfflineStyleError, type OfflineStylePlatform, type OfflineStyleResponse, PLACENAME_LAND_CLASSES, PLACENAME_SOURCE_LAYER, PLACENAME_WATER_CLASSES, type ParsedSigseqTerms, type PmtilesVectorSource, type Poi, type PoiCoverage, type PoiDataset, type PoiOptions, type PoiResponse, type QueryFeatureProperties, type QueryOptions, type QueryResponse, RASTER_ATON_CLASSES, RASTER_GLYPH_NAMES, RASTER_ICON_PREFIX, RELIEF_ATTRIBUTION, RELIEF_ENCODING, RELIEF_REGION, RELIEF_TILE_SIZE, type ResolvedStyle, type ResolvedTheme, type S101ArtworkProvenance, type S101Feature, S101_AREA_FILL_NAMES, S101_CHART_SECTIONS, S101_FEATURES, S101_ICON_PREFIX, S101_LINE_STYLE_NAMES, S101_NEW_SYMBOLS, S101_NO_OFFICIAL_COUNTERPART, S101_SOURCE_COMMIT, S101_SYMBOL_GROUPS, S101_SYMBOL_NAMES, S52_ATON_LOOKUPS, S52_ATON_LOOKUPS_PAPER, S52_DRAW_ORDER, S52_LOOKUP, S52_PATTERN_SPRITE_PREFIX, SIGPER_TOLERANCE_SECONDS, SIGSEQ_MAX_TERMS, SOUNDING_LAYERS, SYMBOL_GROUPS, SYMBOL_MAP, type ScaleBand, type SearchCoverageNotice, type SearchHit, type SearchOptions, type SearchResponse, type SectorArcFeature, type SectorArcFeatureCollection, type SectorInfo, type SigseqRefusal, type SigseqTerm, type SoundingSettings, type SourceMode, type StyleGeneratorOptions, type StyleLayer, type StyleUrlOptions, type SymbolGroup, TileAuth, type TileGroup, type TileGroupsResponse, TileTokenError, type TileTokenErrorCode, type TileTokenResponse, type TransformRequest, type TransformedRequest, type VectorSourceSpec, type VesselProfile, WATER_BODY_LAYER_GROUPS, type WaterBodySettings, WeatherEndpoint, type WeatherReading, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyBaseBundle, applyChrtSymbolSet, applyColorTheme, applyCustomStyleSettings, applyDepthLabelSize, applyEinkPortrayal, applyRasterSymbolSet, applyS101SymbolSet, atonIconExpression, bandFilterExpression, buildAirportTierExpression, buildBuiltUpAreaTierExpression, buildFlashPattern, buildLabelLayer, buildLakeTierExpression, buildLampLayers, buildLandAreaTierExpression, buildLandRegionTierExpression, buildLayersForClass, buildOfflineStyle, buildPlaceNameLayers, buildRiverTierExpression, buildSeaAreaTierExpression, buildSectorArcGeoJSON, buildSectorArcsFC, buildSectorsFromFeatures, buildSources, candidateRank, capLuminance, cellBandCharacters, cellBandMatch, chrtAtonTintExpression, chrtHouseColor, chrtLightTintExpression, collectCandidates, collectClickCandidates, combineWithBandFilter, contrastRatio, daymarkColor, decimateFeatures, decodeAreaFeatureProps, decodeBridgeProps, decodeCandidate, decodeNavAidProps, depareColor, depareFillExpression, depthAreaClassLayerId, depthAreaSplitIds, deriveMarinerName, drapeHalves, drapedFirst, extractNavAidDesignation, featureIdentity, generateCustomStyle, generateSpriteManifest, generateSpriteSheet, generateStyle, generateStyleJSON, getAvailableLineSymbols, getAvailablePatterns, getAvailablePointSymbols, getCollisionSettings, getDrawPriority, getGridSizeForZoom, getIconExpression, getLabelBehavior, getLabelConfig, getLabelPriority, getLookupEntry, getMaxCellsForZoom, getOrderedClasses, getRasterSvg, getS52SvgString, getS52SymbolStats, getStyleStats, getSymbolSvgString, getThemedS101Svg, getThemedS52Svg, getTopmarkMeaning, hazardColor, hazardFillExpression, isDepthAreaClassLayerId, isDrapeable, isLabelLayerVisible, isMapLive, isNavAidLayer, isNavAidLayerVisible, isSoundingLayerVisible, isVisibleInMode, isWaterBodyLayerVisible, labelSortKey, lampRhythmColour, lampRhythmOpacity, lateralColor, lateralFillExpression, lightColor, lightFillExpression, lightsIconExpression, normalizeStyleMode, normalizeTheme, opaqueSplitBaseOf, paginate, parseDepthLabelSize, parseListCodes, parseSigseqTerms, parseWebhook, patternSpriteId, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, rasterAtonIconExpression, relativeLuminance, resolveCustomTheme, resolveRasterImage, resolveS101Color, resolveStyle, resolveTargetClass, rhythmProperties, s101ArtworkProvenance, safetyContourValues, sanitizeCustomStyleConfig, scaleLinearLight, scaleSizeValue, scaminToMinZoom, sectorLimitToArcBearing, splitOpaqueDepthAreas, styleModeBand, terrainStackCount, themeChrtGlyph, tileTemplate, uncoveredSectorArcs, withinBboxFilter, worldMinusBboxPolygon };