@chrtco/sdk 0.10.46 → 0.10.48

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,1582 @@ 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:
2935
+ * ── Why the build has to be IN THE URL ────────────────────────────────────
3019
2936
  *
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).
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.
3027
2943
  *
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.
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.
2950
+ *
2951
+ * ── Why a QUERY PARAMETER and not a path segment ──────────────────────────
2952
+ *
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:
2955
+ *
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`).
2977
+ *
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.
2981
+ *
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).
3031
2984
  */
3032
- declare const DEFAULT_SAFETY_CONTOUR_M = 10;
3033
- declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
2985
+ declare const BUILD_PARAM = "v";
3034
2986
  /**
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.
2987
+ * One tile-URL template — the single place a CHRT tile URL is spelled.
2988
+ *
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.
3038
2994
  */
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;
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;
3004
+ /**
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.
3018
+ */
3019
+ buildTag?: string;
3020
+ /**
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.
3038
+ */
3039
+ buildTags?: Readonly<Record<string, string>>;
3040
+ /** Override tile-worker origin (e.g. staging). Default: DEFAULT_TILE_BASE_URL. */
3041
+ tileBaseUrl?: string;
3042
+ /**
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.
3045
+ */
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;
3060
+ }
3061
+ /**
3062
+ * Build the "sources" block for a CHRT style.
3063
+ *
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`
3068
+ *
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}.
3072
+ */
3073
+ declare function buildSources(opts?: BuildSourcesOptions): Record<string, AnySourceSpec>;
3049
3074
 
3050
3075
  /**
3051
- * Label configurations for S-57 layers.
3052
- * Defines which layers get text labels and how to extract label text.
3076
+ * Builds Mapbox GL Style layer specs from S-52 lookup entries.
3077
+ * Wraps the existing atlas resolveStyle() and adds source/source-layer/filter.
3053
3078
  */
3054
3079
 
3080
+ interface StyleLayer {
3081
+ id: string;
3082
+ type: string;
3083
+ source: string;
3084
+ "source-layer": string;
3085
+ filter?: unknown[];
3086
+ paint: Record<string, unknown>;
3087
+ layout?: Record<string, unknown>;
3088
+ minzoom?: number;
3089
+ maxzoom?: number;
3090
+ metadata?: Record<string, unknown>;
3091
+ }
3055
3092
  /**
3056
- * Feature-class hierarchy for SEAARE labels. Returns a tier number 1-5 per
3057
- * feature:
3058
- *
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.
3093
+ * Build all Mapbox GL layers for a given S-57 object class.
3094
+ * Returns multiple layers (fill, outline, symbol, label, etc.).
3065
3095
  *
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)
3096
+ * Post-consolidation: all S-57 classes live as MVT layers inside one
3097
+ * consolidated PMTiles per region; `source` is always the region name.
3075
3098
  *
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).
3099
+ * `opts.safetyContour` is the mariner's safety depth in metres — it drives
3100
+ * the §CSYM02 isolated-danger ring on hazard classes. Defaults to
3101
+ * `DEFAULT_SAFETY_CONTOUR_M`, the same default the SOUNDG two-shade rule
3102
+ * uses, so the ring and the sounding colour always agree.
3081
3103
  */
3082
- declare function buildSeaAreaTierExpression(): ExpressionSpecification;
3104
+ declare function buildLayersForClass(objectClass: string, ct: ColorTableName, sourceId?: string, opts?: {
3105
+ safetyContour?: number;
3106
+ }): StyleLayer[];
3083
3107
  /**
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)
3088
- *
3089
- * Tier 1/2 may fire via CATBUA when populated; OBJNAM length is the
3090
- * fallback so unpopulated CATBUA still produces a usable hierarchy.
3108
+ * Get the full ordered list of object classes for style generation.
3109
+ * Uses S52_DRAW_ORDER as base, then adds any missing classes from the
3110
+ * S-52 lookup table so a newly-ingested class automatically gets styled
3111
+ * without touching this file.
3091
3112
  */
3092
- declare function buildBuiltUpAreaTierExpression(): ExpressionSpecification;
3113
+ declare function getOrderedClasses(): string[];
3114
+
3093
3115
  /**
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")
3116
+ * CHART SIZING — five sliders over what the chart prints: LAND LABELS, WATER
3117
+ * LABELS, DEPTH NUMBERS, WAYPOINTS, CHART ICONS, 50–200 % (owner, 2026-10-05).
3097
3118
  *
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.
3119
+ * ===========================================================================
3120
+ * ONE MODEL, EVERY CLIENT
3121
+ * ===========================================================================
3122
+ *
3123
+ * The owner asked for the sliders on CHRTsail iOS (*"please add the following
3124
+ * as sizing sliders: land label size, water label size, waypoint size, chart
3125
+ * icon size"*) the same day a captain at a Raspberry Pi helm asked CHRTsail
3126
+ * desktop for *"the depth numbers to be bolder"* (chip 68). The decisions:
3127
+ * desktop and web ADOPT the iOS model rather than ship a control of their own,
3128
+ * and every platform gains a FIFTH slider, DEPTH NUMBERS, for the depth
3129
+ * figures. So this file is a port of `ChartSymbolSizing.swift` (chrt-co/sail,
3130
+ * #1038 at d5cd7060, read 2026-10-05) plus DEPTH NUMBERS as agreed with
3131
+ * CHRTKit's owner (sail, 2026-10-05: `depthNumbers`, `chart.depthNumberScale`,
3132
+ * the ladder and rules below). The classification tables and the collision
3133
+ * rules are copied, not re-derived; a disagreement between the two clients is
3134
+ * a bug in one of them.
3135
+ *
3136
+ * WAYPOINTS is in the vocabulary but not in this file's transform: waypoints
3137
+ * are the app's own marks (DOM, not style layers) and the app scales them.
3138
+ *
3139
+ * ===========================================================================
3140
+ * THE RULE THE PORT KEEPS — iOS's, verbatim in substance
3141
+ * ===========================================================================
3142
+ *
3143
+ * A bigger symbol may OVERLAP the chart. It may never DISPLACE it.
3144
+ *
3145
+ * 1. 100 % is the generated style, byte for byte (`applyChartSymbolScales`
3146
+ * returns the same object).
3147
+ * 2. An enlarged layer stops taking part in other symbols' placement
3148
+ * (`*-ignore-placement: true`): it may vanish itself, it removes nothing.
3149
+ * 3. Enlarged hazard text — CHRT's label band 100, `chrt:labelPriority` —
3150
+ * also gets `text-allow-overlap: true`: never dropped.
3151
+ * 4. The soundings keep CHRT's collision flags (a sounding layer that ignored
3152
+ * placement would stop decluttering itself) — and OUTRANK every label
3153
+ * above them: whenever ANY label slider (land, water, chart icons) is off
3154
+ * 100 %, every text label placed before the soundings, hazard text and
3155
+ * unclassified labels included, stops taking part in placement. A
3156
+ * sounding then gives way only to a shallower sounding. iOS's
3157
+ * `ChartSoundingsRule.outrankLabels`, chosen by its Boston Inner Harbor
3158
+ * census (z13–16, every slider at 50/100/150/200 %): every rule that only
3159
+ * protected the ENLARGED layer lost a shallow sounding, including on the
3160
+ * way DOWN — a sea-area name shrunk to 50 % fitted where it used to be
3161
+ * culled, got placed, and culled a sounding beneath it.
3162
+ *
3163
+ * Its cost, measured on this chart (bench, Boston, LAND 105 %): 46–56 %
3164
+ * more soundings at z11.5–13, aid names up to 3.6x, sea and island names
3165
+ * drawn twice — labels above the soundings stop culling each other as well
3166
+ * as the soundings.
3167
+ *
3168
+ * **THE ZOOM SPLIT — the soundings rule in force** (owner, 2026-10-06;
3169
+ * CHRTKit `ChartSoundingsRule.zoomSplit(harbourFrom: 14)`, sail 20f9f56bb;
3170
+ * `CHART_SOUNDINGS_RULE`). MapLibre has one collision index and no
3171
+ * groups, so a label class above the soundings either culls them or culls
3172
+ * nothing, its own kind included (the pile-up). Rule 4 is therefore kept
3173
+ * only for what orients the chart — sea-area, island and land-region
3174
+ * names, CHRTland and CHRT_BASE labels, and label band 100
3175
+ * (`keptAboveSoundings`). Every OTHER text label above the soundings
3176
+ * (buoy, beacon and fog-signal names, the dredged-area note) moves
3177
+ * directly BELOW them, in its served order, and its collision flags step
3178
+ * with zoom: CHRT's own below zoom 14, where it thins out round the
3179
+ * soundings, and `true` from 14, where it is always drawn and displaces
3180
+ * nothing — exactly what rule 4 draws there. Measured on the bench: 0
3181
+ * shoals uncovered beyond rule 4's own; buoy names 33 / 12 / 5 at
3182
+ * z14 / 15 / 16 against served 25 / 11 / 5, and 5–27 against 60–78 below.
3183
+ * Layer order cannot change with zoom, but flags can, so this is one
3184
+ * order with zoom-stepped flags, the same edits on every platform.
3185
+ *
3186
+ * Shrinking keeps every served flag on the layer that shrinks: a smaller box
3187
+ * can only free space (rule 4 covers what that freed space then admits).
3188
+ *
3189
+ * ===========================================================================
3190
+ * DEPTH NUMBERS — 0.10.3's toggle, as a slider
3191
+ * ===========================================================================
3192
+ *
3193
+ * DEPTH NUMBERS takes the depth figures (`DEPTH_FIGURE_CLASSES`: soundings,
3194
+ * contour values, the depth beside a rock, wreck or obstruction) away from
3195
+ * WATER LABELS and CHART ICONS, and keeps CHRT's collision flags on the
3196
+ * soundings and contour values. OFF 100 %, EITHER WAY, IT TRIGGERS THE ZOOM
3197
+ * SPLIT (owner, 2026-10-06, `labelSlidersOffStandard`: "a size control must
3198
+ * never remove a shoal, whichever way it's moved"). Bigger depth glyphs collide
3199
+ * with more of the labels placed before them: on the bench at Boston DEPTH
3200
+ * 140 % alone left 7 / 3 / 1 shoals uncovered at z12.3 / 13 / 14 — 0.10.3's
3201
+ * Large left 8 / 4 / 1. With the split: 0. Shrinking is a trigger by the same
3202
+ * rule, measured or not (`scripts/chart-bench/depth-numbers-census.ts`).
3203
+ * Above 100 %:
3204
+ *
3205
+ * A. **Depth figures go Semibold** — each served Regular face swapped for its
3206
+ * Semibold (`semiboldFontStack`, derived from the name as CHRTKit does).
3207
+ * Soundings already are; contour values and the depth beside a rock,
3208
+ * wreck or obstruction are Regular, and an enlarged Regular numeral
3209
+ * beside an enlarged Semibold one reads as two kinds of figure. There is no Bold glyph set on any CHRT glyph host
3210
+ * (`cloudflare/tile-worker/glyphs/GLYPHS.json`), so this is the heaviest
3211
+ * face a client may ask for.
3212
+ * B. **Halos grow half as fast as the glyph** (`haloFactor`), on depth
3213
+ * figures only and at DEPTH NUMBERS' own factor — a contour value's
3214
+ * half-rate size does not halve its halo again (CHRTKit,
3215
+ * `ChartDepthFigureRules.haloWidth`). A halo is the S-52 cut-out, and in
3216
+ * DUSK it is dark like a deeper sounding's fill, so it THICKENS that
3217
+ * numeral instead of cutting it out (`s-52.md` §8a). Grown in proportion,
3218
+ * the deeper DUSK soundings out-weighed the shoal ones beside them — the
3219
+ * two-shade rule backwards.
3220
+ * C. **The sounding collision padding shrinks as the soundings grow**
3221
+ * (`soundingPadding`), so about as many soundings are placed at 140 % as
3222
+ * at 100 %: CHRT's 6 px padding at 1.4x placed a quarter fewer (326 → 246
3223
+ * at Boston z14, chip 68); 3.5 px held z11.5–15 within ±5 %.
3224
+ * D. **Contour values grow at half rate** (`contourValueFactor`) — 0.10.3's
3225
+ * 1.2x beside 1.4x soundings, so a contour label still fits its line.
3226
+ * And rule 3 for the depth callouts beside rocks, wrecks and obstructions
3227
+ * (CHRTKit's call): never dropped, never displacing, so a hazard is never
3228
+ * drawn with its depth culled.
3229
+ *
3230
+ * Nothing here touches a label's TEXT, its colours (the §SOUNDG02 two shades),
3231
+ * its sort key, filter or zoom gate, or the layer set.
3102
3232
  */
3103
- declare function buildLandRegionTierExpression(): ExpressionSpecification;
3233
+
3104
3234
  /**
3105
- * Land area tier (LNDARE): major vs. local.
3106
- * 1 — Major (OBJNAM length <= 12)
3107
- * 2 — Local
3235
+ * Drawer order — iOS's `ChartSymbolSize.allCases` plus DEPTH NUMBERS.
3236
+ *
3237
+ * ⚠ DEPTH NUMBERS IS THE OWNER'S FIFTH SLIDER (2026-10-05), on every platform.
3238
+ * It takes the depth figures — soundings, contour values, the depth beside a
3239
+ * rock, wreck or obstruction — away from WATER LABELS and CHART ICONS, scales
3240
+ * them the way 0.10.3's DEPTH NUMBERS toggle did, and off 100 % — either way —
3241
+ * triggers the zoom split as LAND, WATER and CHART ICONS do (owner, 2026-10-06). 100 % on
3242
+ * every client by default. Name, wire and ladder as CHRTKit's shared model;
3243
+ * see `CHART_SYMBOL_SIZE_WIRE`.
3244
+ */
3245
+ declare const CHART_SYMBOL_SIZES: readonly ["landLabels", "waterLabels", "depthNumbers", "waypoints", "chartIcons"];
3246
+ type ChartSymbolSize = (typeof CHART_SYMBOL_SIZES)[number];
3247
+ /** The account key each slider syncs under — iOS `SettingKey.*Scale.wireName`. */
3248
+ declare const CHART_SYMBOL_SIZE_WIRE: Readonly<Record<ChartSymbolSize, string>>;
3249
+ /** The row names — `ChartSymbolSize.label`, verbatim. */
3250
+ declare const CHART_SYMBOL_SIZE_LABELS: Readonly<Record<ChartSymbolSize, string>>;
3251
+ /** What each slider reaches, one line — `ChartSymbolSize.reach`, verbatim. */
3252
+ declare const CHART_SYMBOL_SIZE_REACH: Readonly<Record<ChartSymbolSize, string>>;
3253
+ /** `ChartSymbolScale` — integer percent throughout. */
3254
+ declare const CHART_SYMBOL_SCALE: {
3255
+ readonly min: 50;
3256
+ readonly max: 200;
3257
+ readonly standard: 100;
3258
+ /** The − / + step. */
3259
+ readonly buttonStep: 10;
3260
+ /** The slider's snap. */
3261
+ readonly sliderStep: 5;
3262
+ };
3263
+ type ChartSymbolScales = Readonly<Record<ChartSymbolSize, number>>;
3264
+ declare const STANDARD_CHART_SYMBOL_SCALES: ChartSymbolScales;
3265
+ declare function clampChartSymbolScale(percent: number): number;
3266
+ /** A slider position → the stored percent: nearest 5, clamped. */
3267
+ declare function snapChartSymbolScale(value: number): number;
3268
+ /** One press of − or +, from wherever the value is (125 % − is 115 %). */
3269
+ declare function stepChartSymbolScale(percent: number, presses: number): number;
3270
+ /**
3271
+ * A stored or synced value. Unreadable → `null` (the caller's default);
3272
+ * out of range → clamped, so a newer client's wider range never reaches the
3273
+ * renderer raw. A string must be a whole number — "140", not "140%".
3274
+ */
3275
+ declare function decodeChartSymbolScale(raw: unknown): number | null;
3276
+ /** Text that names a place or a feature on LAND. */
3277
+ declare const LAND_TEXT_CLASSES: ReadonlySet<string>;
3278
+ /**
3279
+ * Text that names WATER, or states a clearance over it. LAKARE, RIVERS and
3280
+ * CANALS are here though CHRT files them under "Land": the label is the name
3281
+ * of water. SOUNDG and DEPCNT left for DEPTH NUMBERS (owner, 2026-10-05).
3282
+ */
3283
+ declare const WATER_TEXT_CLASSES: ReadonlySet<string>;
3284
+ /**
3285
+ * Text that belongs to a charted point symbol — it follows CHART ICONS so it
3286
+ * keeps its em offset from the symbol it annotates.
3287
+ */
3288
+ declare const SYMBOL_ANNOTATION_TEXT_CLASSES: ReadonlySet<string>;
3289
+ /** CHRT's hazard label band. Enlarged text in it is never dropped (rule 3). */
3290
+ declare const HAZARD_LABEL_PRIORITY = 100;
3291
+ /**
3292
+ * The depth figures — DEPTH NUMBERS' text. The labels the depth-unit switch
3293
+ * converts (`buildLabelLayer`): a sounding, a contour value, the depth beside a
3294
+ * rock, wreck or obstruction (that class's ICON stays CHART ICONS'). Not the
3295
+ * dredged-channel note: it is a sentence.
3296
+ */
3297
+ declare const DEPTH_FIGURE_CLASSES: ReadonlySet<string>;
3298
+ /**
3299
+ * Contour values grow at half DEPTH NUMBERS' rate: a contour value rides its
3300
+ * line and needs a straight-enough run to fit, and shares rank 300 with the
3301
+ * soundings, which place first. 0.10.3's Large drew them 1.2x when it drew the
3302
+ * soundings 1.4x, for that reason; this is the same ratio as a slider.
3303
+ */
3304
+ declare function contourValueFactor(depthNumbersFactor: number): number;
3305
+ /**
3306
+ * The font stacks a CHRT glyph host serves (`cloudflare/tile-worker/glyphs/
3307
+ * GLYPHS.json`; the test pins this copy to it). There is no Bold.
3308
+ */
3309
+ declare const SERVED_FONT_STACKS: readonly string[];
3310
+ /**
3311
+ * Rule A: the served font stack with each Regular face swapped for its
3312
+ * Semibold — DERIVED from the served name, not typed, as CHRTKit does, so a
3313
+ * family that changes its face is followed rather than overridden. A swap
3314
+ * that would ask for a face no glyph host serves is refused (the face stays
3315
+ * Regular), as CHRTKit's `servedFontStacks` does. Semibold and Italic faces
3316
+ * are left as served (soundings are already Semibold, so A changes the
3317
+ * contour values and the three callouts). Anything that is not a plain list
3318
+ * of names — absent, or an expression — is returned untouched.
3319
+ */
3320
+ declare function semiboldFontStack(font: unknown): unknown;
3321
+ /** The slider a layer's TEXT follows, or null (stays at 100 %, counted). */
3322
+ declare function textSliderFor(layer: StyleLayer): ChartSymbolSize | null;
3323
+ /** The slider a layer's ICON follows: every chart point symbol, no basemap one. */
3324
+ declare function iconSliderFor(layer: StyleLayer): ChartSymbolSize | null;
3325
+ /**
3326
+ * `value` × `factor`, keeping its zoom structure: the factor goes onto every
3327
+ * stop's OUTPUT, because MapLibre accepts `["zoom"]` only as the input of a
3328
+ * TOP-LEVEL `interpolate`/`step`. `null` for a shape this does not recognise —
3329
+ * the caller leaves that property at its served size rather than guess.
3330
+ * `undefined` (absent) is the renderer default × factor.
3331
+ */
3332
+ declare function scaleSizeExpression(value: unknown, factor: number, fallback: number): unknown;
3333
+ /** Rule B: a halo grows half as fast as its glyph, and only above 100 %. */
3334
+ declare function haloFactor(factor: number): number;
3335
+ /**
3336
+ * CHRT's sounding collision padding, px (`labels.ts`). Read from the generated
3337
+ * layer where it is a number; this is the fallback and the test's anchor.
3338
+ */
3339
+ declare const SERVED_SOUNDING_PADDING_PX = 6;
3340
+ /**
3341
+ * Rule C: px of padding taken off per unit of factor above 1. 6.25 puts 140 %
3342
+ * at 3.5 px — the value the chip-68 sweep chose — and reaches 0 at 196 %.
3343
+ * DEPTH NUMBERS alone (no rule 4) holds the soundings count to −9 % … +7 % of
3344
+ * the served chart across 120–200 %, z11.5–15 (bench, Boston, DAY, 1280×800 at
3345
+ * 1×; −9 % is z14 at 200 %). `.chrt/standards/s-52.md` §8a has the figures.
3346
+ */
3347
+ declare const SOUNDING_PADDING_PER_FACTOR = 6.25;
3348
+ /** Rule C: the sounding padding at a DEPTH NUMBERS factor. */
3349
+ declare function soundingPadding(factor: number, served?: number): number;
3350
+ /** MapLibre's `text-padding` when a layer sets none. */
3351
+ declare const MAPLIBRE_DEFAULT_TEXT_PADDING_PX = 2;
3352
+ /**
3353
+ * Rule C on whatever the served `text-padding` is, as CHRTKit's
3354
+ * `ChartDepthFigureRules.soundingPadding` does: absent is MapLibre's 2 px; a
3355
+ * number is reduced; a zoom `interpolate` or `step` has each OUTPUT reduced;
3356
+ * any other shape is left as served. At or below 100 % it is the served value.
3357
+ */
3358
+ declare function soundingPaddingValue(served: unknown, factor: number): unknown;
3359
+ /**
3360
+ * What the labels above the soundings do when LAND LABELS, WATER LABELS or
3361
+ * CHART ICONS leaves 100 % (the header's rule 4 and THE ZOOM SPLIT). CHRTKit's
3362
+ * `ChartSoundingsRule`, the two cases this client ships.
3363
+ */
3364
+ type ChartSoundingsRule = {
3365
+ readonly kind: "outrankLabels";
3366
+ } | {
3367
+ readonly kind: "zoomSplit";
3368
+ readonly harbourFrom: number;
3369
+ readonly splitLights?: boolean;
3370
+ };
3371
+ /** Rule 4 as iOS first shipped it: every text label above the soundings yields. */
3372
+ declare const OUTRANK_LABELS: ChartSoundingsRule;
3373
+ /** The rule in force — the owner's split at zoom 14 (2026-10-06), CHRTKit's default. */
3374
+ declare const CHART_SOUNDINGS_RULE: ChartSoundingsRule;
3375
+ /**
3376
+ * Whether a label stays above the soundings under the zoom split: label band
3377
+ * 100 (hazard text), the CHRTland and CHRT_BASE labels, and sea-area, island
3378
+ * and land-region names. With `splittingLights`, light characteristics are
3379
+ * split like the buoy names despite being band 100 (measured, not shipped).
3380
+ */
3381
+ declare function keptAboveSoundings(layer: StyleLayer, splittingLights?: boolean): boolean;
3382
+ /** `value` below zoom `z`, `true` from it — CHRTKit `fromZoom`. Absent is MapLibre's `false`. */
3383
+ declare function fromZoom(z: number, value: unknown): unknown[];
3384
+ /**
3385
+ * One generated layer at a set of scales. Returns the SAME object when nothing
3386
+ * reaches it (100 %, unclassified, not a symbol layer, or a size this cannot
3387
+ * multiply), so a standard chart is the generated chart, byte for byte.
3108
3388
  *
3109
- * No category attribute. OBJNAM length is the only signal.
3389
+ * `yieldText` is rule 4, decided by the caller from the layer's position: the
3390
+ * layer is a label placed before the soundings, and some label slider is off
3391
+ * 100 %. Its text then takes no part in placement, whatever its own size.
3110
3392
  */
3111
- declare function buildLandAreaTierExpression(): ExpressionSpecification;
3393
+ declare function applyChartSymbolScalesToLayer(layer: StyleLayer, scales: ChartSymbolScales, yieldText?: boolean): StyleLayer;
3112
3394
  /**
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)
3395
+ * Every layer of a generated style at a set of scales, in draw order — rule 4
3396
+ * reads the order (MapLibre places the TOP of the style first, so "above the
3397
+ * soundings" is "placed before them"). Standard → the same array.
3116
3398
  */
3117
- declare function buildLakeTierExpression(): ExpressionSpecification;
3399
+ declare function applyChartSymbolScales<L extends StyleLayer>(layers: readonly L[], scales: ChartSymbolScales | undefined, rule?: ChartSoundingsRule): L[];
3118
3400
  /**
3119
- * River tier (RIVERS): major vs. local.
3120
- * 1 — Major river (OBJNAM length <= 15 AND no comma)
3121
- * 2 — Local creek (else)
3401
+ * The text labels the zoom split moves below the soundings at these scales,
3402
+ * in draw order — empty when nothing triggers the split
3403
+ * (`labelSlidersOffStandard`: LAND / WATER / DEPTH / ICONS off 100 %, either
3404
+ * way) or the rule does not split. For a client that reorders a live map.
3122
3405
  */
3123
- declare function buildRiverTierExpression(): ExpressionSpecification;
3406
+ declare function demotedBelowSoundings(layers: readonly StyleLayer[], scales: ChartSymbolScales | undefined, rule?: ChartSoundingsRule): string[];
3124
3407
  /**
3125
- * Airport tier (AIRARE): major airport vs. small airstrip.
3126
- * 1 — Major (OBJNAM length <= 25 — e.g. "Logan International Airport")
3127
- * 2 — Airstrip
3408
+ * What no slider reaches, and what a slider reaches but cannot multiply —
3409
+ * iOS's `unclassified` / `unscalable`, for a test that fails when CHRT adds a
3410
+ * label class nobody has decided about.
3128
3411
  */
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
- }
3412
+ declare function chartSymbolSizingCensus(layers: readonly StyleLayer[]): {
3413
+ unclassified: string[];
3414
+ unscalable: string[];
3415
+ };
3416
+
3152
3417
  /**
3153
- * Optional palette-and-data-driven options for label config.
3418
+ * Dominance fan-out collapse (P8 Phase 3.3 / D4 "axis 1").
3154
3419
  *
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.
3420
+ * `makeBandVariants()` historically emitted, per (class, band): one
3421
+ * "not-dominated" layer plus up to five `-domBy{D}` copies that differ ONLY
3422
+ * in their dominance filter and a `maxzoom` of `dominatedVariantMaxzoom(D)`.
3423
+ * That fan existed because MapLibre filters could not read `["zoom"]`. They
3424
+ * can now (style-spec 24.8.5; buckets evaluate at the tile's `overscaledZ`),
3425
+ * so the whole fan folds into ONE layer per (class, band) whose filter
3426
+ * carries the zoom gate.
3427
+ *
3428
+ * **Construction principle — read before changing anything here.** The
3429
+ * collapsed filter is the literal `["any"]` UNION of the clauses the fan
3430
+ * already encoded: every variant the legacy emitter would have produced
3431
+ * contributes exactly one `["all", <its dominance filter>, ["<", ["zoom"],
3432
+ * <its maxzoom>]]` clause, and the dead-variant skip is mirrored too.
3433
+ * Equivalence is therefore *structural*, not re-derived — which matters,
3434
+ * because the two "obvious" hand-written forms are both wrong:
3435
+ *
3436
+ * 1. A `["match", ["get","_dominated_by_min"], 2,5, 3,7, 4,9, 5,11, 6,11, 24]`
3437
+ * threshold (the form the design doc proposed) uses band 6's *eligibility*
3438
+ * zoom 11. The real ceiling is `dominatedVariantMaxzoom("6") = 13` — band
3439
+ * 6's DATA floor. Using 11 reopens the z11–z12 void window that
3440
+ * `band-priority.ts` documents closing.
3441
+ * 2. That same form silently drops the legacy `_dominated: true` boolean,
3442
+ * which `dominatedByMinFilter("6")` still honours. A legacy-tagged feature
3443
+ * has no `_dominated_by_min`, so `match` falls to its default and the
3444
+ * feature renders FOREVER instead of dropping at z13.
3445
+ *
3446
+ * Deriving the gate from the emitter's own clauses avoids both by construction.
3447
+ * `band-collapse.test.ts` re-proves it on every CI run by differentially
3448
+ * evaluating legacy-vs-collapsed through MapLibre's real three gates.
3449
+ *
3450
+ * **Scope.** For FEATURE layers (fills, lines, feature symbols) this is
3451
+ * axis 1 ONLY: the per-band layers survive, with identical ids and identical
3452
+ * positions, so the band-major draw order is untouched. "Axis 2" (collapsing
3453
+ * the per-band copies into one layer per class) was evaluated and **KILLED**
3454
+ * for feature layers — it is unsound for the opaque-fill classes
3455
+ * (`DOMINANCE_EXEMPT_FILL_CLASSES` ≡ `OPAQUE_FILL_LAYERS`), where band-major
3456
+ * order is the ONLY thing separating the bands, and 15/15 class pairs are
3457
+ * provably unsatisfiable with one layer each. Do not reintroduce it there.
3458
+ * Evidence: `.chrt/perf/2026-07-14-p8-d4-spike.md`.
3459
+ *
3460
+ * LABEL layers are different (CHRTsail #121 — MapLibre Native iOS silently
3461
+ * collapses its whole render pass above 482 symbol layers, and the served
3462
+ * style carried 575): `makeCollapsedCrossBandLabel` folds a label's ×6 band
3463
+ * fan into ONE layer per class, and `collapsedOrientationGate` folds the
3464
+ * orientation fan's per-dominator variants into its per-cell layers. That is
3465
+ * NOT the killed axis 2, because what the fill argument protects — paint
3466
+ * order as the sole band separator — does not apply to labels: their
3467
+ * load-bearing order is the cross-class P4 priority ladder (each class keeps
3468
+ * exactly one layer in its ladder slot), and within-class placement priority
3469
+ * rides the `rank*1000 + within-key` symbol-sort-key that labels.ts
3470
+ * documents as designed to survive precisely this merge.
3471
+ *
3472
+ * **Load-bearing invariant.** A collapsed per-band FEATURE layer MUST keep
3473
+ * the `-band{B}` id segment: `custom-style.ts` `bandFloor()` parses
3474
+ * `/-band(\d)/` to clamp customer minzoom/scamin overrides to the band's
3475
+ * data floor, on the live public Style API path. Renaming the id silently
3476
+ * unclamps those overrides. Cross-band LABEL layers are the deliberate
3477
+ * exception: they carry every band floor INSIDE the filter (tile-zoom
3478
+ * clauses a customer override cannot touch), which supersedes the clamp —
3479
+ * `bandFloor()` returning 0 for a bare label id is correct, not a gap.
3480
+ * Both halves asserted in `band-collapse.test.ts`.
3158
3481
  */
3159
- interface LabelConfigOptions {
3160
- /** S-52 colour table — drives palette-aware text and halo colors. */
3161
- colorTable?: ColorTableName;
3482
+
3483
+ /**
3484
+ * Which emitter the style generator uses for the dominance fan.
3485
+ * `"legacy"` exists so CI can differentially prove the two equivalent (it is
3486
+ * the oracle in `band-collapse.test.ts`) and as a one-line rollback. It is an
3487
+ * internal knob — deliberately NOT on the exported `StyleGeneratorOptions`,
3488
+ * so it cannot reach `@chrtco/sdk` consumers via `ChrtStyleOptions`.
3489
+ */
3490
+ type BandFanMode = "collapsed" | "legacy";
3491
+
3492
+ /**
3493
+ * Generates a complete Mapbox GL Style Specification v8 JSON
3494
+ * from the S-52 atlas definitions.
3495
+ */
3496
+
3497
+ interface MapboxStyle {
3498
+ version: 8;
3499
+ name: string;
3500
+ metadata: Record<string, unknown>;
3501
+ sources: Record<string, unknown>;
3502
+ sprite?: string;
3503
+ glyphs: string;
3504
+ layers: StyleLayer[];
3505
+ }
3506
+ interface StyleGeneratorOptions extends BuildSourcesOptions {
3162
3507
  /**
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.
3508
+ * When set, emit a "paper chart" style centered on the selected scale
3509
+ * band, with graceful coverage fallback: the requested band's layers
3510
+ * draw on top where its cells have data; coarser bands (driven by
3511
+ * `CHART_CASCADE_CODES`) fill gaps where the requested band has no
3512
+ * cells. Per-feature SCAMIN minzooms are preserved on every cascade
3513
+ * variant; `maxzoom` is dropped so the user can zoom in indefinitely.
3514
+ *
3515
+ * When omitted (default), continuous mode emits one layer copy per
3516
+ * source band with strict per-band zoom windows so exactly one band
3517
+ * renders at any given continuous zoom.
3518
+ *
3519
+ * `lockedBounds` (optional) restricts chart-mode rendering to a
3520
+ * specific bbox via a `["within"]` filter — applied ONLY to the
3521
+ * requested band's variants. Cascade fallback bands render across the
3522
+ * full viewport so a chart-Harbor user panning offshore still sees
3523
+ * Approach/Coastal data. The frontend pairs this with a translucent
3524
+ * grey overlay outside the bbox so the lock boundary is still visible
3525
+ * to the user (the fallback data is dimmed by the overlay but not
3526
+ * filtered out).
3527
+ */
3528
+ chartMode?: {
3529
+ band: ScaleBand$1;
3530
+ lockedBounds?: BBox;
3531
+ };
3532
+ /**
3533
+ * Display unit for depth labels (SOUNDG soundings, DEPCNT contour
3534
+ * values). Source data is always meters (S-57 / VALSOU / VALDCO);
3535
+ * "feet" wraps the text-field expression with a unit conversion +
3536
+ * round so the rendered numbers match. Default "meters".
3537
+ */
3538
+ depthUnits?: "meters" | "feet";
3539
+ /**
3540
+ * Which symbol artwork the style references. "s52" (default) uses the
3541
+ * PresLib-derived S-52 set; "s101" rewrites icon references to the
3542
+ * official S-101 Portrayal Catalogue symbols (s101: image namespace)
3543
+ * where a counterpart exists — see s101-style.ts. "chrt" applies the
3544
+ * CHRT house ATON/LIGHTS glyphs with colour-true tinting — see
3545
+ * chrt-style.ts. Selection logic differences are documented per module.
3546
+ */
3547
+ symbolSet?: "s52" | "s101" | "chrt" | "raster";
3548
+ /**
3549
+ * The mariner's safety contour in metres — the depth below which a
3550
+ * feature is "dangerous to my hull". Drives both safety-contour
3551
+ * conditional symbology rules:
3552
+ *
3553
+ * - §SOUNDG02 — soundings shoaler than this render in the
3554
+ * high-contrast color; deeper soundings render in the muted color.
3555
+ * - §CSYM02 — isolated dangers (rocks / wrecks / obstructions)
3556
+ * shoaler than this get the DNGCIR01 danger ring.
3557
+ *
3558
+ * Default `DEFAULT_SAFETY_CONTOUR_M` (10 m).
3559
+ *
3560
+ * Does not affect the DEPARE depth-area shading, whose 2/5/10 m bands
3561
+ * are a separate S-52 concept.
3166
3562
  */
3167
3563
  safetyContour?: number;
3168
3564
  /**
3169
- * When false, all soundings render in `SNDG2` (single-shade). Default
3170
- * true — emit the safety-contour case expression.
3565
+ * When false, all soundings render in a single shade. Default true.
3171
3566
  */
3172
3567
  twoShadeSoundings?: boolean;
3173
3568
  /**
3174
- * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
3175
- * numerals at small sizes. Default "semibold".
3569
+ * Label model for the 9 orientation place-name classes (SEAARE, LNDRGN,
3570
+ * LAKARE, RIVERS, CANALS, BUAARE, AIRARE, LNDARE, PRDARE).
3571
+ *
3572
+ * "legacy" (default) — the per-cell-band orientation fan
3573
+ * (fanOrientationLabelByCellBand): one label per feature per band,
3574
+ * deconflicted only by `_label_anchor` tags and watermark opacity.
3575
+ *
3576
+ * "placename" — 2 PLACENAME symbol layers over the build-side
3577
+ * place-name index (CHRTbaremetal placeNameIndex.ts): one collision-
3578
+ * managed label per named place with a per-feature zoom window; the
3579
+ * orientation fan for those classes is NOT emitted. Requires tiles
3580
+ * built with the PLACENAME side index — on older tiles the layers
3581
+ * simply draw nothing (soft-fail is index-side), which is why the
3582
+ * default stays "legacy" until L2c's rebuild + Maine-coast visual
3583
+ * verification flips it (docs/design/chart-label-deconfliction.md
3584
+ * §2.3). Feature-label classes are untouched either way.
3585
+ */
3586
+ labels?: "legacy" | "placename";
3587
+ /**
3588
+ * Font weight for SOUNDG labels. Default "semibold" — crisper numerals
3589
+ * at small sizes on screen.
3176
3590
  */
3177
3591
  soundingFontWeight?: "regular" | "semibold";
3592
+ /**
3593
+ * CHART SIZING — the five sliders, in percent (50–200; `waypoints` is the
3594
+ * app's and is ignored here). Omitted, or every value 100, is byte-identical
3595
+ * to the style without it. Applied LAST, after the symbol-family rewrite, so
3596
+ * it sizes what the chosen family draws. Never changes a label's text,
3597
+ * colours, sort key, filter, zoom gate or the layer set — see
3598
+ * chart-symbol-sizing.ts.
3599
+ */
3600
+ symbolScales?: ChartSymbolScales;
3601
+ /**
3602
+ * What the labels above the soundings do when LAND, WATER or CHART ICONS is
3603
+ * off 100 %, or DEPTH NUMBERS either way — `CHART_SOUNDINGS_RULE` (the owner's zoom split at 14) when
3604
+ * omitted. Rule 4 (`OUTRANK_LABELS`) is kept for measurement.
3605
+ */
3606
+ soundingsRule?: ChartSoundingsRule;
3178
3607
  }
3608
+ declare function generateStyle(palette?: ColorTableName, opts?: StyleGeneratorOptions & {
3609
+ bandFan?: BandFanMode;
3610
+ }): MapboxStyle;
3611
+ declare function generateStyleJSON(palette?: ColorTableName, opts?: StyleGeneratorOptions): string;
3612
+ /** Get summary stats about the generated style */
3613
+ declare function getStyleStats(style: MapboxStyle): {
3614
+ totalLayers: number;
3615
+ fillLayers: number;
3616
+ lineLayers: number;
3617
+ symbolLayers: number;
3618
+ circleLayers: number;
3619
+ sources: number;
3620
+ objectClasses: Set<string>;
3621
+ };
3622
+
3179
3623
  /**
3180
- * Two-tier label rendering strategy for the band-priority style.
3181
- *
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.
3186
- *
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.
3624
+ * The e-ink portrayal pass — what `EINK_COLOR`, `EINK_MONO` and `EINK_NIGHT`
3625
+ * change beyond their colour tables. Night is mono inverted: everything below
3626
+ * applies, the patterns and safety contour are drawn LIGHT (the patterns are
3627
+ * themed per palette, the contour takes the table's DEPSC), and the chart
3628
+ * background gets the no-coverage crosshatch, because a black no-data hole
3629
+ * on a black-water page would read as deep water.
3630
+ *
3631
+ * A colour table can only move colours. E-paper needs more than that (owner
3632
+ * decision 2026-10-05: CHRT serves the e-ink chart, clients never recolour it;
3633
+ * requirements in CHRTsail's e-ink product spec §3c):
3634
+ *
3635
+ * 1. MEANING UNCHANGED. Nothing here adds, removes or reorders a feature
3636
+ * class or changes a filter that decides WHICH features draw. It changes
3637
+ * how they look, and adds two kinds of OVERLAY layer (depth patterns,
3638
+ * the safety contour) that draw features the chart already draws.
3639
+ * 2. MONO STANDS ON ITS OWN. Depth steps become fill PATTERNS (mono only);
3640
+ * danger areas get a cross-hatch; the safety contour gets its own heavy
3641
+ * line; restricted/caution areas keep their dashes, now heavier.
3642
+ * 3. DEPTH IN AT MOST FOUR STEPS. The tables do the colour side (DEPMD ==
3643
+ * DEPDW). Mono shows three: drying (hatch), shallower than the safety
3644
+ * contour (stipple), safe (paper).
3645
+ * 4. SAFETY CONTOUR AND DANGERS EMPHASISED: a heavy black safety-contour
3646
+ * line; danger symbols drawn larger; danger areas cross-hatched.
3647
+ * 5. COVERAGE PORTRAYAL SURVIVES: the no-coverage crosshatch goes to full
3648
+ * strength rather than being dropped with the other translucency.
3649
+ * 6. ATTRIBUTION/CURRENCY METADATA: untouched — this pass never edits
3650
+ * `sources` or the metadata other than adding `chrt:eink`.
3651
+ * 7. SIZES FOR 150 ppi COLOUR / 300 ppi MONO: every line ×1.5 (min 1 px),
3652
+ * text ×1.15 with a solid paper halo, no translucency, no blur, no fades.
3653
+ *
3654
+ * Translucency is the thing e-paper cannot draw: a 10% magenta wash over pale
3655
+ * blue is a fifth grey in mono and nothing at all on Kaleido 3. So faint washes
3656
+ * (< 0.3) are dropped — every one of them is a CHRT addition over a feature
3657
+ * whose boundary and symbol S-52 already draws — and stronger ones are made
3658
+ * solid.
3659
+ *
3660
+ * Pure: takes a generated style, returns a new one. `generateStyle` applies it
3661
+ * for the e-ink tables, so the served style, the offline style and the SDK's
3662
+ * local `generateStyle` all carry the same portrayal.
3195
3663
  */
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;
3664
+
3665
+ /** The three e-ink fill patterns (house glyphs, registered with the world hatches). */
3666
+ declare const EINK_DOTS_PATTERN = "chrt:eink-dots";
3667
+ declare const EINK_HATCH_PATTERN = "chrt:eink-hatch";
3668
+ declare const EINK_DANGER_PATTERN = "chrt:eink-danger";
3669
+ /** The VALDCO values that ARE the safety contour, one per ladder. */
3670
+ declare function safetyContourValues(safetyContourM: number): number[];
3671
+ interface EinkPortrayalOptions {
3672
+ /** The mariner's safety contour, metres — the same value the generator used. */
3673
+ safetyContour?: number;
3674
+ }
3200
3675
  /**
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.
3205
- *
3206
- * Rank semantics: LOWER rank = higher priority. The rank drives two
3207
- * mechanisms that must stay in agreement:
3208
- *
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.)
3218
- *
3219
- * The ladder (safety-critical > nav-aids > depth field > structures >
3220
- * ambient notes):
3221
- *
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.
3235
- *
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.
3676
+ * Apply the e-ink portrayal for `palette`. Returns the style unchanged for a
3677
+ * non-e-ink palette, so a caller may apply it unconditionally.
3251
3678
  */
3252
- declare function labelSortKey(rank: number, within?: unknown): ExpressionSpecification | number;
3679
+ declare function applyEinkPortrayal(style: MapboxStyle, palette: ColorTableName, opts?: EinkPortrayalOptions): MapboxStyle;
3680
+
3253
3681
  /**
3254
- * Extract the trailing designation token from a nav-aid OBJNAM.
3682
+ * Light arithmetic for the chart palettes: how much light a colour emits, and
3683
+ * how to dim a colour without changing its hue.
3255
3684
  *
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).
3685
+ * Everything here works in LINEAR light (WCAG 2.x relative luminance, sRGB
3686
+ * primaries). That is the quantity a dark-adapted eye responds to, and it is
3687
+ * the one `sounding-legibility.test.ts` already measures soundings in. Scaling
3688
+ * the sRGB-ENCODED channels instead would dim dark colours far more than
3689
+ * bright ones and shift every hue it touched.
3260
3690
  *
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).
3691
+ * The one derivation the palettes rely on, and the reason this file exists:
3692
+ * NIGHT's deeper sounding SNDG1 (#897350) is SNDG2 (#c8aa78) at 43% of its
3693
+ * linear light. `labels.ts` builds its NIGHT secondary text the same way, so
3694
+ * the two stay one family by construction rather than by two hand-typed hexes
3695
+ * that happen to agree.
3264
3696
  */
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;
3697
+ /** WCAG 2.x relative luminance of a `#rrggbb` colour, 0 (black) to 1 (white). */
3698
+ declare function relativeLuminance(hex: string): number;
3699
+ /** WCAG 2.x contrast ratio between two `#rrggbb` colours, 1 to 21. */
3700
+ declare function contrastRatio(a: string, b: string): number;
3268
3701
  /**
3269
- * Build a Mapbox symbol layer spec from a label config.
3702
+ * The same hue at `factor` times its linear light. `factor` above 1 can clip a
3703
+ * channel, which shifts the hue; nothing in the palettes brightens, so callers
3704
+ * pass 0–1.
3270
3705
  */
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;
3706
+ declare function scaleLinearLight(hex: string, factor: number): string;
3276
3707
  /**
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.
3708
+ * `hex` dimmed, hue kept, until its luminance is at most `ceiling`. A colour
3709
+ * already at or under the ceiling comes back unchanged (lower-cased).
3280
3710
  */
3281
- declare function preprocessSoundings(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3711
+ declare function capLuminance(hex: string, ceiling: number): string;
3282
3712
  /**
3283
- * Pre-process LIGHTS GeoJSON to derive compact light characteristic labels.
3284
- * Adds `_lightLabel` property (e.g., "Fl(2) WR 4s 5M").
3713
+ * NIGHT's two ink levels, shared by every family's text and marks.
3285
3714
  *
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.
3290
- */
3291
- declare function preprocessLights(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
3292
- /** List of layers that have labels */
3293
- declare const LABELED_LAYERS: Set<string>;
3294
- /**
3295
- * Pre-process buoy/beacon GeoJSON to derive concise mariner-style labels.
3296
- * Adds `_navAidLabel` property (e.g., 'R "8"', 'G "5"').
3297
- */
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;
3301
- /**
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.
3304
- */
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[];
3312
- /**
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.
3715
+ * `NIGHT_INK` S52_NIGHT.CHBLK — the palette's linework and navigation
3716
+ * text. The brightest thing NIGHT draws on purpose.
3717
+ * `NIGHT_MUTED_INK` that ink at 43% of its linear light, which is how
3718
+ * S52_NIGHT.SNDG1 (the deeper soundings) was derived.
3719
+ * Everything that orients rather than navigates is drawn
3720
+ * at or under this, and `night-luminance.test.ts` holds
3721
+ * every visible NIGHT colour to it unless an exemption
3722
+ * names a reason.
3320
3723
  */
3321
- declare function buildPlaceNameLayers(sourceId: string, ct?: ColorTableName): maplibregl.LayerSpecification[];
3724
+ declare const NIGHT_INK: string;
3725
+ declare const NIGHT_MUTED_INK: string;
3322
3726
 
3323
3727
  /**
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.
3728
+ * Is this MapLibre map still there to be written to?
3341
3729
  *
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
3347
- */
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;
3354
- /**
3355
- * Get the max number of cells to load based on zoom level.
3356
- */
3357
- declare function getMaxCellsForZoom(zoom: number): number;
3358
-
3359
- /**
3360
- * IHO S-52 / NOAA Chart No. 1 / INT1 nautical symbol SVGs.
3730
+ * ===========================================================================
3731
+ * ⚠ ASK AFTER EVERY AWAIT. A MAP CAN BE REMOVED WHILE YOU WERE WAITING.
3732
+ * ===========================================================================
3361
3733
  *
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
3734
+ * `map.remove()` deletes `map.style`, and after that nearly every method that
3735
+ * touches the style throws rather than returning: `hasImage`, `addImage`,
3736
+ * `updateImage`, `getSource`, `addLayer` all fail with "Cannot read properties
3737
+ * of undefined (reading 'getImage')" and its siblings. Code that was handed a
3738
+ * map, awaited a decode, an IndexedDB read or a fetch, and then wrote to the
3739
+ * map, throws from wherever it resumed — usually a promise nobody is awaiting,
3740
+ * so the throw is an uncaught error with no caller to explain it.
3366
3741
  *
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
3742
+ * Measured in CHRTdesktop (chip 63, 2026-10-04): with the page's main thread
3743
+ * held at 6× CPU, the chart removed its map in the middle of a pan, and the
3744
+ * S-52 image loader resumed onto it:
3375
3745
  *
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) ⊗
3746
+ * TypeError: Cannot read properties of undefined (reading 'getImage')
3747
+ * at Map.hasImage
3748
+ * at addImageToMap (s52-image-cache.ts)
3381
3749
  *
3382
- * Each symbol is a compact SVG at 32×32 for Mapbox GL `map.addImage()`.
3750
+ * The rule, for anything in this package or the app that takes a map: after
3751
+ * an `await` (or in a `.then`, a frame callback, an `onload`), check this
3752
+ * before the first call on the map, and stop quietly if it says no. A removed
3753
+ * map is not an error to report — whoever removed it is building the next one.
3754
+ *
3755
+ * ⚠ `_removed`, NOT `style`. maplibre sets `_removed = true` as the last step
3756
+ * of `remove()` (typed on `Map` in maplibre-gl 4 and 5), so it is the fact,
3757
+ * where `style === undefined` is a consequence. Test doubles that never had a
3758
+ * `style` read as live, which is what they are. The app pins the maplibre side
3759
+ * of this (`map-live.test.ts`), so an upgrade that renames the field fails a
3760
+ * test rather than silently making every check say yes.
3761
+ *
3762
+ * Typed `object` so the structural map shapes the app passes around (a tile
3763
+ * census, an errored-tile tracker) can be asked too, not only `maplibregl.Map`.
3383
3764
  */
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;
3765
+ declare function isMapLive(map: object): boolean;
3766
+
3767
+ /** S-52 display categories – controls default visibility */
3768
+ type DisplayCategory = "BASE" | "STANDARD" | "OTHER" | "MARINERS" | "META";
3769
+ /** Mapbox GL layer type we'll use for this entry */
3770
+ type GeomType = "fill" | "line" | "circle" | "symbol";
3771
+ /** Nautical groupings (loosely follows Chart No. 1 sections) */
3772
+ type NauticalCategory = "Depths" | "Land" | "Navigation Aids" | "Hazards" | "Regulated Areas" | "Infrastructure" | "Meta";
3773
+ /** Paint properties – a subset we actually use */
3774
+ interface AtlasStyle {
3775
+ type: GeomType;
3776
+ paint: Record<string, unknown>;
3777
+ layout?: Record<string, unknown>;
3778
+ /** Nautical symbol ID for symbol-type layers */
3779
+ iconImage?: string;
3396
3780
  }
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[];
3781
+ /** One row in the S-52 lookup table */
3782
+ interface LookupEntry {
3783
+ /** S-57 object class acronym */
3784
+ objectClass: string;
3785
+ /** Human-readable name */
3786
+ name: string;
3787
+ /** Chart No. 1 section */
3788
+ category: NauticalCategory;
3789
+ /** S-52 display category */
3790
+ displayCategory: DisplayCategory;
3791
+ /** Default style using S-52 color tokens (resolved later) */
3792
+ style: AtlasStyle;
3793
+ /** Whether conditional symbology may override the style */
3794
+ hasConditional?: boolean;
3795
+ /** Attributes that affect styling */
3796
+ conditionalAttributes?: string[];
3797
+ /** INT1 / Chart No. 1 reference (e.g. "Q130.1") */
3798
+ int1Ref?: string;
3799
+ /** S-52 viewing group number */
3800
+ viewingGroup?: number;
3801
+ /** Minimum compilation scale for display (SCAMIN) */
3802
+ scamin?: number;
3803
+ /**
3804
+ * Mark this class as deliberately invisible (label-only or metadata-only)
3805
+ * so the layer-coverage audit doesn't flag its geometry-type gaps as bugs.
3806
+ *
3807
+ * Examples:
3808
+ * - SOUNDG: rendered as depth numbers via labels.ts, not as visible dots
3809
+ * - LNDRGN: region NAME labels only, no shape
3810
+ * - M_NPUB / M_NSYS / M_QUAL / M_SREL / DSID: S-57 metadata classes;
3811
+ * present for click-query attributes, never drawn
3812
+ *
3813
+ * Without this flag, `scripts/audit-layer-coverage.ts` reports the
3814
+ * thousands of features in these classes as "gaps" every week even
3815
+ * though the invisibility is intentional.
3816
+ */
3817
+ auditIgnore?: boolean;
3449
3818
  }
3450
- /** Chart No. 1 section metadata */
3451
- interface ChartSection {
3452
- id: string;
3453
- label: string;
3454
- description: string;
3819
+ /** Resolved style ready for Mapbox GL (tokens → hex) */
3820
+ interface ResolvedStyle {
3821
+ type: GeomType;
3822
+ paint: Record<string, unknown>;
3823
+ layout?: Record<string, unknown>;
3824
+ minzoom?: number;
3825
+ maxzoom?: number;
3455
3826
  }
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.
3827
+
3828
+ /**
3829
+ * S-52 lookup table covering all S-57 object classes found in NOAA ENCs.
3830
+ * Colours reference S-52 tokens (resolved at render time).
3831
+ * INT1 references per NOAA Chart No. 1 (Ed 13).
3461
3832
  */
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/.
3833
+ declare const S52_LOOKUP: Record<string, LookupEntry>;
3834
+
3835
+ /**
3836
+ * S-52 draw order: layers listed bottom → top.
3837
+ * Layers not in this list are inserted before point features.
3465
3838
  */
3466
- declare function getIconExpression(layerName: string): string | unknown[];
3839
+ declare const S52_DRAW_ORDER: string[];
3840
+ /** Get numeric draw priority for a layer (lower = drawn first / below) */
3841
+ declare function getDrawPriority(layerName: string): number;
3842
+
3843
+ /** S-52 display modes */
3844
+ type DisplayMode = "BASE" | "STANDARD" | "ALL";
3845
+ /** Check if a layer's display category is visible in the given mode */
3846
+ declare function isVisibleInMode(displayCategory: DisplayCategory, mode: DisplayMode): boolean;
3467
3847
  /**
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()`).
3848
+ * Convert S-52 SCAMIN (scale minimum) to Mapbox GL minzoom.
3471
3849
  */
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
- }>;
3850
+ declare function scaminToMinZoom(scamin?: number): number | undefined;
3851
+ declare function getCollisionSettings(viewingGroup?: number): {
3852
+ iconAllowOverlap: boolean;
3853
+ iconOptional: boolean;
3854
+ symbolSortKey?: number;
3855
+ };
3856
+ /**
3857
+ * Get the resolved Mapbox GL style for a given S-57 layer.
3858
+ */
3859
+ declare function resolveStyle(layerName: string, geomKind?: GeomType, ct?: ColorTableName, opts?: {
3860
+ safetyContour?: number;
3861
+ }): ResolvedStyle;
3862
+ declare function getLookupEntry(layerName: string): LookupEntry;
3489
3863
 
3490
3864
  /**
3491
- * IHO S-101 ENC Product Specification — Feature Catalogue
3865
+ * S-52 Conditional Symbology Procedures.
3866
+ * Each function takes feature properties and returns paint overrides (hex colours).
3867
+ * All expression builders accept an optional colorTable parameter.
3868
+ */
3869
+ /**
3870
+ * Default safety contour in metres — the depth below which a feature is
3871
+ * "dangerous to my hull" for portrayal purposes.
3492
3872
  *
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)
3873
+ * This is the single source of truth for the *default* used by every
3874
+ * safety-contour-driven conditional symbology rule, so the rules can't
3875
+ * silently disagree with each other:
3498
3876
  *
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
3877
+ * - §SOUNDG02 — soundings shoaler than the contour get the
3878
+ * high-contrast shade (see `getLabelConfig` in labels.ts).
3879
+ * - §CSYM02 — isolated dangers shoaler than the contour get the
3880
+ * DNGCIR01 ring (see `buildLayersForClass` in layer-builder.ts).
3881
+ * - §DEPARE04 — the MS/MD shade boundary (owner decision 2026-08-21,
3882
+ * replacing a hardcoded 5 m middle cut; see the DEPARE block below
3883
+ * for the full IHO correspondence).
3504
3884
  *
3505
- * Reference: IHO S-101 Product Specification (Ed 1.1.0)
3506
- * IHO S-100 Universal Hydrographic Data Model
3885
+ * All are overridden by the user's setting (sounding panel slider,
3886
+ * `safetyContour` in the style options); this is only the fallback when
3887
+ * none is supplied. It matches `custom-style.ts`'s config default.
3507
3888
  */
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
- }[];
3542
-
3889
+ declare const DEFAULT_SAFETY_CONTOUR_M = 10;
3890
+ declare function depareColor(props: Record<string, unknown>, ct?: ColorTableName, safetyContourM?: number): string;
3543
3891
  /**
3544
- * S-101 Portrayal Catalogue — New Symbol SVGs
3545
- *
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.
3549
- *
3550
- * Reference: IHO S-101 Portrayal Catalogue (Ed 1.1.0)
3892
+ * The colour token of each `case` branch of `depareFillExpression`, in branch
3893
+ * order, then the fallback. `depth-area-opaque.ts` names the layers it splits
3894
+ * the expression into from this list, so the two read from one place.
3551
3895
  */
3896
+ declare const DEPARE_FILL_BRANCH_TOKENS: readonly ["DEPIT", "DEPVS", "DEPMS", "DEPMD"];
3897
+ declare const DEPARE_FILL_FALLBACK_TOKEN = "DEPDW";
3898
+ declare function depareFillExpression(ct?: ColorTableName, safetyContourM?: number): unknown[];
3899
+ declare function lightColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3900
+ declare function lightFillExpression(ct?: ColorTableName): unknown[];
3901
+ declare function lateralColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3902
+ declare function lateralFillExpression(ct?: ColorTableName): unknown[];
3903
+ declare function hazardColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3904
+ declare function hazardFillExpression(ct?: ColorTableName): unknown[];
3905
+ declare function daymarkColor(props: Record<string, unknown>, ct?: ColorTableName): string;
3552
3906
 
3553
- declare const S101_NEW_SYMBOLS: NauticalSymbol[];
3907
+ /**
3908
+ * Label configurations for S-57 layers.
3909
+ * Defines which layers get text labels and how to extract label text.
3910
+ */
3554
3911
 
3555
3912
  /**
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.
3913
+ * Feature-class hierarchy for SEAARE labels. Returns a tier number 1-5 per
3914
+ * feature:
3560
3915
  *
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.
3916
+ * Exported (with the other tier builders below) for the build-side parity
3917
+ * test: CHRTbaremetal replicates these taxonomies as data in
3918
+ * tools/chrt-baremetal/server/src/tilebuild/placeNameTiers.ts to stamp
3919
+ * `tier` onto PLACENAME index features, and
3920
+ * placename-tier-parity.test.ts evaluates the two against shared
3921
+ * fixtures. Change a branch here and that test names the drift.
3922
+ *
3923
+ * 1 — Ocean (OBJNAM contains "<X> Ocean" — CATSEA has no
3924
+ * ocean code; the attribute is 1/unset on oceans)
3925
+ * 2 — Sea / Gulf (OBJNAM "Gulf of …" / "… Sea")
3926
+ * 3 — Bay / Sound / (CATSEA in {5 bay, 12 narrows} OR OBJNAM
3927
+ * Strait / Narrows " Bay" / " Sound" / " Strait")
3928
+ * 4 — Anchorage detail (OBJNAM contains "Anchorage Area")
3929
+ * 5 — Relief + default (CATSEA relief codes — authoritative — or
3930
+ * relief-term names, or anything uncategorised;
3931
+ * suppressed below z9 by the size curve)
3932
+ *
3933
+ * Order matters: tier-4/5 demotions (anchorages, relief) run BEFORE the
3934
+ * promotion shims so "Bear Seamount" (contains " Sea") and "…Anchorage
3935
+ * Area" compound names can't bubble up into headline tiers.
3936
+ * `coalesce(CATSEA, catsea)` handles post-tippecanoe case variation
3937
+ * (matches `s52-conditional.ts` precedent).
3564
3938
  */
3565
- /** Vector source emitted for the legacy Mapbox MTS path. */
3566
- interface MapboxVectorSource {
3567
- type: "vector";
3568
- url: string;
3569
- }
3939
+ declare function buildSeaAreaTierExpression(): ExpressionSpecification;
3570
3940
  /**
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.
3941
+ * Built-up area tier (BUAARE): major city / town / neighborhood.
3942
+ * 1 — Major city (CATBUA=1 urban, or OBJNAM length <= 8)
3943
+ * 2 — Town (CATBUA=5 village, or OBJNAM length <= 14)
3944
+ * 3 — Neighborhood (longer compound names)
3945
+ *
3946
+ * Tier 1/2 may fire via CATBUA when populated; OBJNAM length is the
3947
+ * fallback so unpopulated CATBUA still produces a usable hierarchy.
3574
3948
  */
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
- }
3949
+ declare function buildBuiltUpAreaTierExpression(): ExpressionSpecification;
3583
3950
  /**
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.
3951
+ * Land region tier (LNDRGN): major region vs. local point.
3952
+ * 1 — Major region (OBJNAM length <= 12 AND no comma — "Long Island", "Cape Cod")
3953
+ * 2 — Local point (else — "Bird Island", "Cottage Park")
3954
+ *
3955
+ * No CATLND analog for region scope, so OBJNAM is the only signal.
3956
+ * The comma-absence test is a heuristic: NOAA compound names like
3957
+ * "Boston Harbor, MA Castle Island" carry a comma and belong at deeper
3958
+ * zoom regardless of length.
3588
3959
  */
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";
3960
+ declare function buildLandRegionTierExpression(): ExpressionSpecification;
3607
3961
  /**
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).
3962
+ * Land area tier (LNDARE): major vs. local.
3963
+ * 1 — Major (OBJNAM length <= 12)
3964
+ * 2 — Local
3965
+ *
3966
+ * No category attribute. OBJNAM length is the only signal.
3615
3967
  */
3616
- declare const BASE_MINZOOM = 0;
3617
- declare const BASE_MAXZOOM = 11;
3968
+ declare function buildLandAreaTierExpression(): ExpressionSpecification;
3618
3969
  /**
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.
3970
+ * Lake tier (LAKARE): lake vs. reservoir/pond.
3971
+ * 1 — Lake (CATLAK=1, or OBJNAM length <= 12)
3972
+ * 2 — Reservoir/pond (CATLAK in {2,3}, or longer name)
3626
3973
  */
3627
- declare const BASE_ATTRIBUTION = "\u00A9 CHRT \u00B7 Made with Natural Earth \u00B7 \u00A9 OpenStreetMap contributors";
3974
+ declare function buildLakeTierExpression(): ExpressionSpecification;
3628
3975
  /**
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`).
3976
+ * River tier (RIVERS): major vs. local.
3977
+ * 1 — Major river (OBJNAM length <= 15 AND no comma)
3978
+ * 2 — Local creek (else)
3635
3979
  */
3636
- declare const LAND_ATTRIBUTION = "\u00A9 OpenStreetMap contributors";
3980
+ declare function buildRiverTierExpression(): ExpressionSpecification;
3637
3981
  /**
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.
3982
+ * Airport tier (AIRARE): major airport vs. small airstrip.
3983
+ * 1 — Major (OBJNAM length <= 25 — e.g. "Logan International Airport")
3984
+ * 2 — Airstrip
3641
3985
  */
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;
3986
+ declare function buildAirportTierExpression(): ExpressionSpecification;
3987
+ interface LabelLayerConfig {
3988
+ /** Mapbox source layer type */
3989
+ type: "symbol";
3990
+ /** Text field expression or string */
3991
+ textField: ExpressionSpecification | string;
3992
+ /** Text size in pixels */
3993
+ textSize: number | ExpressionSpecification;
3994
+ /** Text color (hex) or data-driven expression */
3995
+ textColor: string | ExpressionSpecification;
3996
+ /** Halo color for contrast */
3997
+ textHaloColor: string;
3998
+ /** Halo width — pixels or zoom-interpolation expression */
3999
+ textHaloWidth: number | ExpressionSpecification;
4000
+ /** Text offset [x, y] in ems */
4001
+ textOffset: [number, number];
4002
+ /** Font stack */
4003
+ textFont?: string[];
4004
+ /** Additional layout properties */
4005
+ layout?: Record<string, unknown>;
4006
+ /** Minimum zoom level for this label */
4007
+ minzoom?: number;
4008
+ }
3645
4009
  /**
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.
4010
+ * Optional palette-and-data-driven options for label config.
4011
+ *
4012
+ * Currently used by SOUNDG to pick palette-aware text/halo colors and to
4013
+ * drive the S-52 safety-contour two-shade rule. Other classes ignore
4014
+ * these options.
3650
4015
  */
3651
- declare const RELIEF_ENCODING = "terrarium";
3652
- type SourceMode = "mapbox" | "pmtiles";
4016
+ interface LabelConfigOptions {
4017
+ /** S-52 colour table — drives palette-aware text and halo colors. */
4018
+ colorTable?: ColorTableName;
4019
+ /**
4020
+ * Safety contour depth in metres. Soundings shoaler than this render in
4021
+ * `SNDG2` (high-contrast); deeper soundings render in `SNDG1` (muted).
4022
+ * S-52 §SOUNDG02 conditional symbology.
4023
+ */
4024
+ safetyContour?: number;
4025
+ /**
4026
+ * When false, all soundings render in `SNDG2` (single-shade). Default
4027
+ * true — emit the safety-contour case expression.
4028
+ */
4029
+ twoShadeSoundings?: boolean;
4030
+ /**
4031
+ * When "semibold", SOUNDG labels use Open Sans Semibold for crisper
4032
+ * numerals at small sizes. Default "semibold".
4033
+ */
4034
+ soundingFontWeight?: "regular" | "semibold";
4035
+ }
3653
4036
  /**
3654
- * Query parameter carrying the resolved build id on every tile URL.
3655
- *
3656
- * ── Why the build has to be IN THE URL ────────────────────────────────────
4037
+ * Two-tier label rendering strategy for the band-priority style.
3657
4038
  *
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.
4039
+ * `feature-band-gated` — emit one variant per source band, each gated to
4040
+ * its band's zoom window (the existing `makeBandVariants` behaviour). Right
4041
+ * for dense feature labels where seeing the same buoy designation or
4042
+ * sounding twice across overlapping bands is visual noise.
3664
4043
  *
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.
4044
+ * `orientation-cross-band` — emit a SINGLE label layer with no band
4045
+ * filter. The per-class `minzoom` is honoured. Right for orientation
4046
+ * "where am I" labels (sea-area names, land regions, cities) — these
4047
+ * must persist as the user zooms in, even if the broader-scope cell
4048
+ * carrying the OBJNAM is from a coarser band than the one rendering
4049
+ * features at the current zoom. Collision arbitration via
4050
+ * `ORIENTATION_BAND_SORT_KEY` gives the broader-scope (lower-band) copy
4051
+ * priority where the same OBJNAM exists in multiple bands.
4052
+ */
4053
+ type LabelBehavior = "feature-band-gated" | "orientation-cross-band";
4054
+ declare const LABEL_BEHAVIOR: Record<string, LabelBehavior>;
4055
+ /** Returns the rendering strategy for an S-57 class. Unknown classes default to band-gated. */
4056
+ declare function getLabelBehavior(layerName: string): LabelBehavior;
4057
+ /**
4058
+ * Class-based label priority ladder — THE single collision-priority model
4059
+ * for feature labels (portrayal-excellence P4). Converges the previous
4060
+ * per-mechanism patches (SAFETY_CRITICAL_LABELS allow-overlap, the
4061
+ * band-5/6-only Harbor collision discipline) into one explicit ranking.
3671
4062
  *
3672
- * ── Why a QUERY PARAMETER and not a path segment ──────────────────────────
4063
+ * Rank semantics: LOWER rank = higher priority. The rank drives two
4064
+ * mechanisms that must stay in agreement:
3673
4065
  *
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:
4066
+ * 1. `symbol-sort-key` — every feature label carries
4067
+ * `rank*1000 + within-class-key`, so if label layers are ever merged
4068
+ * (the P8 style fan-out reduction), placement priority survives the
4069
+ * merge unchanged.
4070
+ * 2. Label LAYER ORDER — MapLibre places symbols topmost-layer-first, so
4071
+ * `style-generator.ts` emits feature-label layers lowest-priority
4072
+ * first: safety-critical classes end up topmost and claim collision
4073
+ * space before anything else. (`symbol-sort-key` alone cannot do
4074
+ * this — it only orders placement WITHIN a layer.)
3676
4075
  *
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`).
4076
+ * The ladder (safety-critical > nav-aids > depth field > structures >
4077
+ * ambient notes):
3698
4078
  *
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.
4079
+ * 100 — Safety-critical: light characteristics (LIGHTS), hazard depth
4080
+ * callouts (UWTROC/OBSTRN/WRECKS VALSOU), overhead clearances
4081
+ * (CBLOHD/PIPOHD VERCLR). Irreducible mariner-safety text.
4082
+ * 200 — Nav-aid designations: buoy / beacon `R "4"` labels.
4083
+ * 250 — Auxiliary nav-aid info: fog signal type (FOGSIG).
4084
+ * 300 — Depth field: soundings, contour values. Individually droppable —
4085
+ * a hidden sounding has visible neighbors.
4086
+ * 400 — Structures: bridges, landmarks, piers, moorings, buildings.
4087
+ * 500 — Ambient annotation: seabed nature, magnetic variation, kelp.
4088
+ * 900 — Orientation names (SEAARE etc.) — not part of this ladder;
4089
+ * they keep their settled watermark model (cell-band fan +
4090
+ * dominance composition + `orientationLabelLayout`) and always
4091
+ * paint underneath feature labels.
3702
4092
  *
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).
4093
+ * Safety guardrail: a lower rung never blocks a higher rung — placement
4094
+ * order guarantees the safety-critical label got the space first. Density
4095
+ * relief comes from dropping the LOWEST rung in a pile-up, never the top.
3705
4096
  */
3706
- declare const BUILD_PARAM = "v";
4097
+ declare const LABEL_PRIORITY: Record<string, number>;
3707
4098
  /**
3708
- * One tile-URL template — the single place a CHRT tile URL is spelled.
3709
- *
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.
4099
+ * Resolve the priority rank for an S-57 class's label. Orientation classes
4100
+ * report 900 regardless of ladder membership (PRDARE etc.) so the
4101
+ * layer-order sort in style-generator.ts can treat priority uniformly.
3715
4102
  */
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;
3763
- /**
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.
3766
- */
3767
- land?: boolean;
3768
- /**
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.
3772
- */
3773
- relief?: boolean;
3774
- /**
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().
3779
- */
3780
- base?: boolean;
3781
- }
4103
+ declare function getLabelPriority(layerName: string): number;
4104
+ /**
4105
+ * Compose a `symbol-sort-key` from a class rank and an optional
4106
+ * within-class key expression. The within-class key is clamped to
4107
+ * [0, 999] so it can never bleed into a neighboring rank's band.
4108
+ */
4109
+ declare function labelSortKey(rank: number, within?: unknown): ExpressionSpecification | number;
3782
4110
  /**
3783
- * Build the "sources" block for a CHRT style.
4111
+ * Extract the trailing designation token from a nav-aid OBJNAM.
3784
4112
  *
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`
4113
+ * NOAA names channel aids "<waterway> <type> Buoy|Beacon <designation>"
4114
+ * where the designation is 1-4 alphanumerics: "4", "18A", "B", "BG".
4115
+ * Returns null when the name has no designation (trailing token is a
4116
+ * stopword type-name, or longer than 4 chars).
3789
4117
  *
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}.
4118
+ * This is the TS twin of `navAidDesignationTextExpression` — the GeoJSON
4119
+ * preprocessing path uses this function, the PMTiles path uses the GL
4120
+ * expression. Keep the two in agreement (see label-priority.test.ts).
3793
4121
  */
3794
- declare function buildSources(opts?: BuildSourcesOptions): Record<string, AnySourceSpec>;
3795
-
4122
+ declare function extractNavAidDesignation(objnam: string): string | null;
4123
+ /** Get label config for a given S-57 layer, or null if no labels */
4124
+ declare function getLabelConfig(layerName: string, opts?: LabelConfigOptions): LabelLayerConfig | null;
3796
4125
  /**
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
- * ===========================================================================
3807
- *
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.
3812
- *
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:
3815
- *
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).
3829
- *
3830
- * ===========================================================================
3831
- * WHICH LABELS ARE "DEPTH NUMBERS"
3832
- * ===========================================================================
3833
- *
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.
3841
- *
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.
3845
- *
3846
- * ===========================================================================
3847
- * WHY NOT A BOLD FACE
3848
- * ===========================================================================
4126
+ * Build a Mapbox symbol layer spec from a label config.
4127
+ */
4128
+ declare function buildLabelLayer(layerId: string, sourceId: string, config: LabelLayerConfig, visible: boolean, overrides?: {
4129
+ textScale?: number;
4130
+ minzoom?: number | null;
4131
+ allowOverlap?: boolean;
4132
+ }): maplibregl.LayerSpecification;
4133
+ /**
4134
+ * Pre-process SOUNDG GeoJSON to extract Z coordinates as `depth` properties.
4135
+ * Soundings are MultiPoint with Z values in geometry.coordinates[][2].
4136
+ * This explodes MultiPoints into individual Points with a `depth` property.
4137
+ */
4138
+ declare function preprocessSoundings(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
4139
+ /**
4140
+ * Pre-process LIGHTS GeoJSON to derive compact light characteristic labels.
4141
+ * Adds `_lightLabel` property (e.g., "Fl(2) WR 4s 5M").
3849
4142
  *
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.
4143
+ * KEEP IN STEP WITH `buildLightLabelExpression()` in layer-builder.ts — that
4144
+ * is the same label built as a GL expression for the tile path, which has no
4145
+ * JS preprocessing step. `light-label-parity.test.ts` asserts the two agree
4146
+ * on real tile features; if you change one, change both.
4147
+ */
4148
+ declare function preprocessLights(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
4149
+ /** List of layers that have labels */
4150
+ declare const LABELED_LAYERS: Set<string>;
4151
+ /**
4152
+ * Pre-process buoy/beacon GeoJSON to derive concise mariner-style labels.
4153
+ * Adds `_navAidLabel` property (e.g., 'R "8"', 'G "5"').
4154
+ */
4155
+ declare function preprocessNavAids(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
4156
+ /** Check if a layer name needs nav aid preprocessing */
4157
+ declare function isNavAidLayer(layerName: string): boolean;
4158
+ /**
4159
+ * Pre-process water body polygon GeoJSON into centroid points for label placement.
4160
+ * Computes a simple average-coordinate centroid for each polygon feature that has OBJNAM.
4161
+ */
4162
+ declare function preprocessWaterBodies(fc: GeoJSON.FeatureCollection): GeoJSON.FeatureCollection;
4163
+ /** MVT layer the build-side place-name index emits. */
4164
+ declare const PLACENAME_SOURCE_LAYER = "PLACENAME";
4165
+ /** Orientation classes whose names render italic (water voice). */
4166
+ declare const PLACENAME_WATER_CLASSES: readonly string[];
4167
+ /** Orientation classes whose names render upright (land voice). */
4168
+ declare const PLACENAME_LAND_CLASSES: readonly string[];
4169
+ /**
4170
+ * The 2 PLACENAME symbol layers. Layout contract (design §2.2): collision
4171
+ * ON (`allow-overlap: false`, `ignore-placement: false`,
4172
+ * `optional: true`), padding 4, `text-variable-anchor`, and
4173
+ * `symbol-sort-key` straight from the build-side `rank` — MapLibre places
4174
+ * LOWER sort keys first, and rank is minted lower-is-more-important
4175
+ * (tier major, bbox area minor), so when space runs out the biggest names
4176
+ * survive. That ranking is the per-zoom density governor P4 asked for.
3857
4177
  */
4178
+ declare function buildPlaceNameLayers(sourceId: string, ct?: ColorTableName): maplibregl.LayerSpecification[];
3858
4179
 
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
4180
  /**
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.
4181
+ * Spatial grid-based feature decimation for performance at low zoom levels.
4182
+ * Thins dense GeoJSON feature sets by keeping only one representative
4183
+ * feature per grid cell per layer.
3866
4184
  */
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>;
4185
+ interface DecimationOptions {
4186
+ /** Grid resolution (cells per axis). Default 80. */
4187
+ gridSize?: number;
4188
+ /** Zoom level below which decimation kicks in. Default 12. */
4189
+ zoomThreshold?: number;
4190
+ }
3872
4191
  /**
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.
4192
+ * Zoom-adaptive grid resolution for decimation.
3891
4193
  */
3892
- declare const LARGE_SOUNDING_PADDING_PX = 3.5;
4194
+ declare function getGridSizeForZoom(zoom: number): number;
3893
4195
  /**
3894
- * Apply a depth-label size to one generated label layer.
4196
+ * Decimate a GeoJSON FeatureCollection using spatial grid thinning.
4197
+ * Only runs when zoom < zoomThreshold. Returns the original FC otherwise.
3895
4198
  *
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.
4199
+ * @param fc - Input FeatureCollection
4200
+ * @param layerName - S-57 layer name (affects priority logic)
4201
+ * @param currentZoom - Current map zoom level
4202
+ * @param bounds - Current viewport bounds
4203
+ * @param opts - Decimation options
3899
4204
  */
3900
- declare function applyDepthLabelSize(objectClass: string, layer: StyleLayer, size: DepthLabelSize | undefined): StyleLayer;
3901
-
4205
+ declare function decimateFeatures(fc: GeoJSON.FeatureCollection, layerName: string, currentZoom: number, bounds: {
4206
+ west: number;
4207
+ south: number;
4208
+ east: number;
4209
+ north: number;
4210
+ }, opts?: DecimationOptions): GeoJSON.FeatureCollection;
3902
4211
  /**
3903
- * Builds Mapbox GL Style layer specs from S-52 lookup entries.
3904
- * Wraps the existing atlas resolveStyle() and adds source/source-layer/filter.
4212
+ * Get the max number of cells to load based on zoom level.
3905
4213
  */
4214
+ declare function getMaxCellsForZoom(zoom: number): number;
3906
4215
 
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
4216
  /**
3920
- * Build all Mapbox GL layers for a given S-57 object class.
3921
- * Returns multiple layers (fill, outline, symbol, label, etc.).
4217
+ * IHO S-52 / NOAA Chart No. 1 / INT1 nautical symbol SVGs.
3922
4218
  *
3923
- * Post-consolidation: all S-57 classes live as MVT layers inside one
3924
- * consolidated PMTiles per region; `source` is always the region name.
4219
+ * Complete symbology suite following:
4220
+ * - IHO S-52 Presentation Library (Ed 4.0)
4221
+ * - IALA Maritime Buoyage System R1001 (Region B for US waters)
4222
+ * - NOAA Chart No. 1 (Ed 13) / INT1
3925
4223
  *
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.
4224
+ * Buoy shape terminology (US / Chart No. 1):
4225
+ * - CAN (Q20): Flat-top cylindrical buoy — Port mark in IALA-B (red)
4226
+ * - NUN (Q21): Conical/pointed-top buoy — Starboard mark in IALA-B (green)
4227
+ * - PILLAR: Tall narrow buoy — used for cardinal, isolated danger, etc.
4228
+ * - SPAR: Tall thin pole buoy
4229
+ * - SPHERICAL: Round buoy — safe water marks
4230
+ * - BARREL: Barrel/tun shaped buoy
4231
+ * - SUPER: Large navigation buoy / LANBY
4232
+ *
4233
+ * Cardinal mark topmark orientations (IALA):
4234
+ * - North: both cones point UP ▲▲
4235
+ * - South: both cones point DOWN ▼▼
4236
+ * - East: cones base-to-base (diamond) ◆
4237
+ * - West: cones point-to-point (hourglass) ⊗
4238
+ *
4239
+ * Each symbol is a compact SVG at 32×32 for Mapbox GL `map.addImage()`.
3930
4240
  */
3931
- declare function buildLayersForClass(objectClass: string, ct: ColorTableName, sourceId?: string, opts?: {
3932
- safetyContour?: number;
3933
- }): StyleLayer[];
4241
+ /** Color used in the SVG with its S-52 token name, hex, and usage */
4242
+ interface SymbolColor {
4243
+ token: string;
4244
+ hex: string;
4245
+ usage: string;
4246
+ }
4247
+ /** Line style used in the SVG */
4248
+ interface SymbolLineStyle {
4249
+ element: string;
4250
+ width: number;
4251
+ pattern?: string;
4252
+ color: string;
4253
+ }
4254
+ /** Fill element used in the SVG */
4255
+ interface SymbolFillElement {
4256
+ element: string;
4257
+ color: string;
4258
+ opacity?: number;
4259
+ }
4260
+ interface NauticalSymbol {
4261
+ id: string;
4262
+ label: string;
4263
+ /** INT1 / Chart No. 1 reference */
4264
+ int1Ref: string;
4265
+ /** Chart No. 1 section letter */
4266
+ section: string;
4267
+ getSvg: () => string;
4268
+ /** Generate SVG with theme-specific colors (for bridge icons etc.) */
4269
+ getThemedSvg?: (chblk: string, movable: string) => string;
4270
+ /** What this symbol represents on a chart */
4271
+ description?: string;
4272
+ /** S-52 Presentation Library symbol name */
4273
+ s52PlRef?: string;
4274
+ /** S-57 object class (e.g. "LNDMRK") when it can't be derived from s52PlRef's prefix */
4275
+ s57Class?: string;
4276
+ /** Grouping key for family clustering */
4277
+ group?: string;
4278
+ /** IDs of related variant symbols */
4279
+ variants?: string[];
4280
+ /** S-57 attribute that selects this variant */
4281
+ attributeKey?: string;
4282
+ /** Value of that attribute for this variant */
4283
+ attributeValue?: number;
4284
+ /** Example label strings seen on charts */
4285
+ labelExamples?: string[];
4286
+ /** What the color represents */
4287
+ colorMeaning?: string;
4288
+ /** Colors used in the SVG */
4289
+ colors?: SymbolColor[];
4290
+ /** Line/stroke styles used in the SVG */
4291
+ lineStyles?: SymbolLineStyle[];
4292
+ /** Fill regions in the SVG */
4293
+ fillElements?: SymbolFillElement[];
4294
+ /** Expanded navigational usage context (2-4 sentences) */
4295
+ usageContext?: string;
4296
+ /** Render size for documentation */
4297
+ renderSize?: string;
4298
+ }
4299
+ /** A logical grouping of related symbols */
4300
+ interface SymbolGroup {
4301
+ id: string;
4302
+ label: string;
4303
+ section: string;
4304
+ description: string;
4305
+ symbolIds: string[];
4306
+ }
4307
+ /** Chart No. 1 section metadata */
4308
+ interface ChartSection {
4309
+ id: string;
4310
+ label: string;
4311
+ description: string;
4312
+ }
4313
+ declare const NAUTICAL_SYMBOLS: NauticalSymbol[];
4314
+ /** S-57 object class → default symbol ID mapping.
4315
+ * Values are S-52 PL symbol names (matching SVG filenames in s52/point/).
4316
+ * Conditional expressions in getIconExpression() override these for classes
4317
+ * with attribute-dependent variants.
4318
+ */
4319
+ declare const SYMBOL_MAP: Record<string, string>;
4320
+ /** Get the Mapbox icon-image expression for data-driven symbol selection.
4321
+ * Returns S-52 PL symbol names that correspond to SVG files in s52/point/.
4322
+ */
4323
+ declare function getIconExpression(layerName: string): string | unknown[];
3934
4324
  /**
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.
4325
+ * Browser-only image loaders (`loadSymbolImages`, `loadThemedBridgeImages`)
4326
+ * live in `./symbols-browser.ts` (sub-export `@chrtco/sdk/atlas/browser`) —
4327
+ * they require a DOM (use `new Image()` + `map.addImage()`).
3939
4328
  */
3940
- declare function getOrderedClasses(): string[];
4329
+ /** Get inline SVG string for a symbol by ID (for the Atlas legend) */
4330
+ declare function getSymbolSvgString(symbolId: string): string | null;
4331
+ declare const CHART_SECTIONS: ChartSection[];
4332
+ declare const SYMBOL_GROUPS: SymbolGroup[];
4333
+ /** Generate a single SVG sprite sheet containing all symbols as <symbol> elements */
4334
+ declare function generateSpriteSheet(): string;
4335
+ /** Generate a JSON manifest mapping symbol IDs to metadata */
4336
+ declare function generateSpriteManifest(): Record<string, {
4337
+ label: string;
4338
+ int1Ref: string;
4339
+ section: string;
4340
+ group?: string;
4341
+ s52PlRef?: string;
4342
+ description?: string;
4343
+ attributeKey?: string;
4344
+ attributeValue?: number;
4345
+ }>;
3941
4346
 
3942
4347
  /**
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.
4348
+ * IHO S-101 ENC Product Specification — Feature Catalogue
3974
4349
  *
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`.
4350
+ * S-101 replaces S-57 as the data model for Electronic Navigational Charts.
4351
+ * This catalogue maps S-101 feature classes to:
4352
+ * - S-57 equivalents (where they exist)
4353
+ * - Chart No. 1 sections (same organizational structure)
4354
+ * - Portrayal catalogue references (S-100 Portrayal Model)
3984
4355
  *
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.
4356
+ * Key S-101 changes from S-57:
4357
+ * - CamelCase feature class names (DepthArea vs DEPARE)
4358
+ * - Complex/structured attributes (nested objects vs flat)
4359
+ * - New feature types for AIS, quality, under-keel clearance
4360
+ * - Removed/merged features for simplification
3996
4361
  *
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`.
4362
+ * Reference: IHO S-101 Product Specification (Ed 1.1.0)
4363
+ * IHO S-100 Universal Hydrographic Data Model
4014
4364
  */
4015
- type BandFanMode = "collapsed" | "legacy";
4365
+ interface S101Feature {
4366
+ /** S-101 camelCase feature class name */
4367
+ featureClass: string;
4368
+ /** Human-readable name */
4369
+ name: string;
4370
+ /** S-57 6-char equivalent acronym, if any */
4371
+ s57Equivalent?: string;
4372
+ /** Chart No. 1 section letter */
4373
+ section: string;
4374
+ /** Group key for clustering */
4375
+ group: string;
4376
+ /** Feature category */
4377
+ category: "geo" | "meta" | "info";
4378
+ /** Description of the feature */
4379
+ description: string;
4380
+ /** S-100 Portrayal Catalogue reference */
4381
+ portrayalRef?: string;
4382
+ /** True if this feature has no S-57 equivalent */
4383
+ isNewInS101: boolean;
4384
+ /** Key attributes specific to this feature */
4385
+ attributes?: string[];
4386
+ }
4387
+ declare const S101_FEATURES: S101Feature[];
4388
+ declare const S101_CHART_SECTIONS: {
4389
+ id: string;
4390
+ label: string;
4391
+ description: string;
4392
+ }[];
4393
+ declare const S101_SYMBOL_GROUPS: {
4394
+ id: string;
4395
+ label: string;
4396
+ section: string;
4397
+ description: string;
4398
+ }[];
4016
4399
 
4017
4400
  /**
4018
- * Generates a complete Mapbox GL Style Specification v8 JSON
4019
- * from the S-52 atlas definitions.
4401
+ * S-101 Portrayal Catalogue — New Symbol SVGs
4402
+ *
4403
+ * These symbols represent features introduced in S-101 that have no
4404
+ * direct S-57/S-52 equivalent. The existing S-52 symbols are reused
4405
+ * where S-101 retains visual continuity with the S-57 portrayal.
4406
+ *
4407
+ * Reference: IHO S-101 Portrayal Catalogue (Ed 1.1.0)
4020
4408
  */
4021
4409
 
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
- };
4410
+ declare const S101_NEW_SYMBOLS: NauticalSymbol[];
4140
4411
 
4141
4412
  declare const S101_SOURCE_COMMIT = "05063500e898544c90078ab78a61efc72b7e62f3";
4142
4413
 
@@ -4847,7 +5118,11 @@ declare const DEFAULT_CUSTOM_STYLE_CONFIG: CustomStyleConfig;
4847
5118
  * fields fall back to the platform defaults. Never throws.
4848
5119
  */
4849
5120
  declare function sanitizeCustomStyleConfig(raw: unknown): CustomStyleConfig;
4850
- /** Normalize a `?theme=` value to a colour table name; null for anything else. */
5121
+ /**
5122
+ * Normalize a `?theme=` value to a colour table name; null for anything else.
5123
+ * Case-insensitive; `-` is accepted for `_` (`eink-mono` = `eink_mono`), so
5124
+ * the e-ink names survive either spelling a caller guesses.
5125
+ */
4851
5126
  declare function normalizeTheme(raw: string | null | undefined): ColorTableName | null;
4852
5127
  interface ResolvedTheme {
4853
5128
  palette: ColorTableName;
@@ -5215,7 +5490,8 @@ declare class OfflineError extends Error {
5215
5490
 
5216
5491
  /** Options for `chrt.style()`. Extends the atlas style generator options. */
5217
5492
  interface ChrtStyleOptions extends StyleGeneratorOptions {
5218
- /** S-52 colour palette: "DAY" (default), "DUSK", or "NIGHT". */
5493
+ /** Colour palette: "DAY" (default), "DUSK", "NIGHT", or the e-paper
5494
+ * palettes "EINK_COLOR" / "EINK_MONO" / "EINK_NIGHT" (see `EINK_COLOR_TABLES`). */
5219
5495
  palette?: ColorTableName;
5220
5496
  }
5221
5497
  declare class Chrt {
@@ -5504,4 +5780,4 @@ declare class Chrt {
5504
5780
  warmTiles(region?: string): Promise<TileTokenResponse>;
5505
5781
  }
5506
5782
 
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 };
5783
+ 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, CHART_SOUNDINGS_RULE, CHART_SYMBOL_SCALE, CHART_SYMBOL_SIZES, CHART_SYMBOL_SIZE_LABELS, CHART_SYMBOL_SIZE_REACH, CHART_SYMBOL_SIZE_WIRE, 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, type ChartSoundingsRule, type ChartSymbolScales, type ChartSymbolSize, 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_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_FIGURE_CLASSES, DRAPEABLE_LAYER_TYPES, type DecodedAreaFeature, type DecodedBridge, type DecodedNavAid, type DecodedProperty, 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, HAZARD_LABEL_PRIORITY, 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, LAND_TEXT_CLASSES, type LabelBehavior, type LabelConfigOptions, type LabelLayerConfig, type LabelSettings, type LampLayer, type LookupEntry, MAPLIBRE_DEFAULT_TEXT_PADDING_PX, 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, OUTRANK_LABELS, 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, SERVED_FONT_STACKS, SERVED_SOUNDING_PADDING_PX, SIGPER_TOLERANCE_SECONDS, SIGSEQ_MAX_TERMS, SOUNDING_LAYERS, SOUNDING_PADDING_PER_FACTOR, STANDARD_CHART_SYMBOL_SCALES, SYMBOL_ANNOTATION_TEXT_CLASSES, 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, WATER_TEXT_CLASSES, type WaterBodySettings, WeatherEndpoint, type WeatherReading, type WebhookEvent, type WebhookEventType, type WebhookPayload, applyBaseBundle, applyChartSymbolScales, applyChartSymbolScalesToLayer, applyChrtSymbolSet, applyColorTheme, applyCustomStyleSettings, 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, chartSymbolSizingCensus, chrtAtonTintExpression, chrtHouseColor, chrtLightTintExpression, clampChartSymbolScale, collectCandidates, collectClickCandidates, combineWithBandFilter, contourValueFactor, contrastRatio, daymarkColor, decimateFeatures, decodeAreaFeatureProps, decodeBridgeProps, decodeCandidate, decodeChartSymbolScale, decodeNavAidProps, demotedBelowSoundings, depareColor, depareFillExpression, depthAreaClassLayerId, depthAreaSplitIds, deriveMarinerName, drapeHalves, drapedFirst, extractNavAidDesignation, featureIdentity, fromZoom, 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, haloFactor, hazardColor, hazardFillExpression, iconSliderFor, isDepthAreaClassLayerId, isDrapeable, isLabelLayerVisible, isMapLive, isNavAidLayer, isNavAidLayerVisible, isSoundingLayerVisible, isVisibleInMode, isWaterBodyLayerVisible, keptAboveSoundings, labelSortKey, lampRhythmColour, lampRhythmOpacity, lateralColor, lateralFillExpression, lightColor, lightFillExpression, lightsIconExpression, normalizeStyleMode, normalizeTheme, opaqueSplitBaseOf, paginate, parseListCodes, parseSigseqTerms, parseWebhook, patternSpriteId, preprocessLights, preprocessNavAids, preprocessSoundings, preprocessWaterBodies, rasterAtonIconExpression, relativeLuminance, resolveCustomTheme, resolveRasterImage, resolveS101Color, resolveStyle, resolveTargetClass, rhythmProperties, s101ArtworkProvenance, safetyContourValues, sanitizeCustomStyleConfig, scaleLinearLight, scaleSizeExpression, scaleSizeValue, scaminToMinZoom, sectorLimitToArcBearing, semiboldFontStack, snapChartSymbolScale, soundingPadding, soundingPaddingValue, splitOpaqueDepthAreas, stepChartSymbolScale, styleModeBand, terrainStackCount, textSliderFor, themeChrtGlyph, tileTemplate, uncoveredSectorArcs, withinBboxFilter, worldMinusBboxPolygon };