@chrtco/sdk 0.10.19 → 0.10.21

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.js CHANGED
@@ -366,8 +366,8 @@ var RELIEF_REGION = "CHRT_RELIEF";
366
366
  var BASE_REGION = "CHRT_BASE";
367
367
  var BASE_MINZOOM = 0;
368
368
  var BASE_MAXZOOM = 11;
369
- var RELIEF_MINZOOM = 8;
370
- var RELIEF_MAXZOOM = 11;
369
+ var RELIEF_MINZOOM = 9;
370
+ var RELIEF_MAXZOOM = 12;
371
371
  var RELIEF_TILE_SIZE = 512;
372
372
  var BUILD_PARAM = "v";
373
373
  function tileTemplate(base, region, extension, buildId) {
@@ -22829,14 +22829,19 @@ var ORIENTATION_BAND_SORT_KEY = [
22829
22829
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
22830
22830
  9
22831
22831
  ];
22832
- function orientationLabelLayout(extras = {}) {
22832
+ function orientationSortKey(tier) {
22833
+ const band = ["*", 100, ORIENTATION_BAND_SORT_KEY];
22834
+ if (!tier) return band;
22835
+ return ["+", band, tier];
22836
+ }
22837
+ function orientationLabelLayout(extras = {}, tier) {
22833
22838
  return {
22834
22839
  "text-anchor": "center",
22835
22840
  "text-allow-overlap": false,
22836
22841
  "text-ignore-placement": false,
22837
22842
  "text-optional": true,
22838
22843
  "text-padding": 15,
22839
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY,
22844
+ "symbol-sort-key": orientationSortKey(tier),
22840
22845
  ...extras
22841
22846
  };
22842
22847
  }
@@ -22846,15 +22851,17 @@ function tieredSizeCurve(tierExpr, stops) {
22846
22851
  if (i > 0) {
22847
22852
  const [pz, pv] = stops[i - 1];
22848
22853
  const [nz, nv] = stops[i];
22849
- if (pv.some((v, t) => v === 0 && nv[t] > 0)) {
22854
+ if (pv.some((v, t) => v === 0 && nv[t] > 0 || v > 0 && nv[t] === 0)) {
22850
22855
  const hz = Math.round((nz - 0.2) * 10) / 10;
22851
22856
  if (hz > pz) {
22852
22857
  const f = (hz - pz) / (nz - pz);
22853
22858
  withHolds.push([
22854
22859
  hz,
22855
- pv.map(
22856
- (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
22857
- )
22860
+ pv.map((v, t) => {
22861
+ if (v === 0 && nv[t] > 0) return 0;
22862
+ if (v > 0 && nv[t] === 0) return v;
22863
+ return Math.round((v + (nv[t] - v) * f) * 10) / 10;
22864
+ })
22858
22865
  ]);
22859
22866
  }
22860
22867
  }
@@ -22883,6 +22890,19 @@ var ORIENTATION_HALO_WIDTH = [
22883
22890
  16,
22884
22891
  0.7
22885
22892
  ];
22893
+ var LAND_HALO_WIDTH = [
22894
+ "interpolate",
22895
+ ["linear"],
22896
+ ["zoom"],
22897
+ 8,
22898
+ 1.6,
22899
+ 11,
22900
+ 1.7,
22901
+ 14,
22902
+ 1.8,
22903
+ 16,
22904
+ 1.8
22905
+ ];
22886
22906
  var LABEL_BEHAVIOR = {
22887
22907
  // Orientation labels — render once across all bands.
22888
22908
  SEAARE: "orientation-cross-band",
@@ -22956,6 +22976,9 @@ var LABEL_PRIORITY = {
22956
22976
  };
22957
22977
  var DEFAULT_LABEL_PRIORITY = 450;
22958
22978
  var ORIENTATION_LABEL_PRIORITY = 900;
22979
+ var MAJOR_WATER_TIER_MAX = 3;
22980
+ var MAJOR_WATER_LABEL_PRIORITY = 280;
22981
+ var ISLAND_LABEL_HOIST_MINZOOM = 12;
22959
22982
  function getLabelPriority(layerName) {
22960
22983
  if (getLabelBehavior(layerName) === "orientation-cross-band") {
22961
22984
  return ORIENTATION_LABEL_PRIORITY;
@@ -23128,21 +23151,25 @@ function getLabelConfig(layerName, opts = {}) {
23128
23151
  // LNDRGN labels until coastal zoom. The Carto basemap already labels
23129
23152
  // major land regions at z5-z6. Columns: t1 major region, t2 local
23130
23153
  // point (also the default).
23154
+ // Tier 2 (local point: "Bird Island", "Cottage Park") gets the same
23155
+ // lift as LNDARE's small islands — it is the other half of "show me
23156
+ // what this land is". Tier 1 is unchanged.
23131
23157
  textSize: tieredSizeCurve(tierExpr, [
23132
23158
  [5, [0, 0]],
23133
23159
  [7, [11, 0]],
23134
23160
  [9, [12, 0]],
23135
- [11, [12, 9]],
23136
- [13, [11, 11]],
23137
- [16, [10, 10]]
23161
+ [11, [12, 10]],
23162
+ [13, [11.5, 11.5]],
23163
+ [16, [10.5, 11.5]]
23138
23164
  ]),
23139
23165
  textColor: DARK_BG,
23140
23166
  textHaloColor: COLOR.LAND_REGION,
23141
- textHaloWidth: ORIENTATION_HALO_WIDTH,
23167
+ // Land halo, not the shared taper — see LAND_HALO_WIDTH.
23168
+ textHaloWidth: LAND_HALO_WIDTH,
23142
23169
  textOffset: [0, 0],
23143
23170
  textFont: ["Open Sans Regular"],
23144
23171
  minzoom: 5,
23145
- layout: orientationLabelLayout()
23172
+ layout: orientationLabelLayout({}, tierExpr)
23146
23173
  };
23147
23174
  }
23148
23175
  case "LNDMRK":
@@ -23307,14 +23334,56 @@ function getLabelConfig(layerName, opts = {}) {
23307
23334
  // at approach scale (z9) when the user has zoomed in enough that
23308
23335
  // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
23309
23336
  // t3 bay/sound, t4 anchorage, default.
23337
+ // ── The zoom ladder (owner, 2026-08-31) ────────────────────────────
23338
+ //
23339
+ // "When zoomed in I want water area labels (Nubble Channel etc.); as
23340
+ // I zoom out, the major stuff."
23341
+ //
23342
+ // The old curve gave EVERY tier a size at z9-z11, and the field that
23343
+ // produced is the defect. Rendered in isolation at Boston z10.4
23344
+ // (prod 48404b1d): ten copies of "North Atlantic Ocean" scattered
23345
+ // across the harbour, a dozen ledges and shoals — Finns, Boston,
23346
+ // Thieves, Ultonia, Harding, Tinkers, Dread, Newcomb, Three and
23347
+ // One-Half Fathom — and, competing with all of them for the same
23348
+ // pixels, the bays the user actually needs: Broad Sound, Dorchester,
23349
+ // Quincy, Hull, Hingham, President Roads. Nothing was missing. The
23350
+ // majors simply had no better claim on the space than a ledge did.
23351
+ //
23352
+ // So the ladder now steps tiers OUT as well as in:
23353
+ //
23354
+ // t1 ocean overview only — gone by z11. "North Atlantic
23355
+ // Ocean" inside Boston Harbor is noise, not context.
23356
+ // t2 sea/gulf through approach — gone by z13.
23357
+ // t3 bay/sound the spine: visible z5 through z16, peaking where
23358
+ // a bay IS the answer to "where am I".
23359
+ // t4 anchorage harbour detail, z13+ (unchanged).
23360
+ // t5 default channels, coves, ledges. Floor UNCHANGED at z9 —
23361
+ // and deliberately so. "Nubble Channel" is already
23362
+ // a z11 feature without touching this curve: NOAA
23363
+ // encodes it only in band-5 cells (US5BOSCD ×2,
23364
+ // US5BOSCE), band-5 features first exist in tiles at
23365
+ // z11, and the cell-band fan floors their label
23366
+ // variant at z11 too. The owner's "channels/coves at
23367
+ // z11+" is therefore satisfied by the DATA, and
23368
+ // moving this floor would only have cost the
23369
+ // offshore relief its approach-zoom entry (Georges
23370
+ // Bank, Hudson Canyon — the population this tier was
23371
+ // built for, pinned by seaare-tier.test.ts).
23372
+ //
23373
+ // What crowds the bays at z10 is fixed by RANK, not by floors:
23374
+ // `orientationSortKey` now places tier 3 ahead of tier 5 inside a
23375
+ // band, and the padding ladder below thins the field so that
23376
+ // ordering decides the survivors.
23377
+ //
23378
+ // Columns: t1 ocean, t2 sea/gulf, t3 bay/sound, t4 anchorage, default.
23310
23379
  textSize: tieredSizeCurve(tierExpr, [
23311
23380
  [3, [14, 8, 0, 0, 0]],
23312
23381
  [5, [16, 13, 9, 0, 0]],
23313
23382
  [7, [14, 14, 12, 0, 0]],
23314
- [9, [12, 13, 14, 0, 9]],
23315
- [11, [10, 12, 13, 0, 11]],
23316
- [13, [9, 11, 12, 10, 11]],
23317
- [16, [9, 10, 11, 11, 10]]
23383
+ [9, [11, 13, 14, 0, 9]],
23384
+ [11, [0, 11, 14, 0, 11]],
23385
+ [13, [0, 0, 13, 10, 11]],
23386
+ [16, [0, 0, 12, 11, 10]]
23318
23387
  ]),
23319
23388
  textColor: DARK_BG,
23320
23389
  textHaloColor: COLOR.WATER_DEEP,
@@ -23338,8 +23407,41 @@ function getLabelConfig(layerName, opts = {}) {
23338
23407
  // the tiles and replaces this fan outright — do not restore
23339
23408
  // always-paint on a starvation report; flip `labels: "placename"`
23340
23409
  // instead once the build carries the layer.
23341
- "text-padding": 2
23342
- })
23410
+ //
23411
+ // ── The padding is the zoom LADDER (owner, 2026-08-31) ───────────
23412
+ //
23413
+ // "When zoomed in I want water area labels (Nubble Channel etc.);
23414
+ // as I zoom out, the major stuff."
23415
+ //
23416
+ // A flat 2 px moat gives every name in view the same claim on the
23417
+ // space, so at z10 all 34 band-4 names in one Boston tile compete
23418
+ // as peers and the result reads as clutter rather than as
23419
+ // orientation. Widening the moat as the user zooms OUT thins that
23420
+ // field by collision alone: with a 20 px moat only a few names
23421
+ // survive per screen, and `orientationSortKey` decides which —
23422
+ // lowest band first, then lowest class tier, i.e. the bays and
23423
+ // sounds. Narrowing it back to 2 px by z13 restores exactly
23424
+ // today's harbour behaviour, where the channels and coves should
23425
+ // all be readable.
23426
+ //
23427
+ // Same mechanism (and the same lesson) as the lake-label grid:
23428
+ // a flat padding let six copies of "Lake Ontario" crowd z8.9.
23429
+ // Nothing is filtered and no name is invented — the ladder is
23430
+ // entirely a collision-space decision.
23431
+ "text-padding": [
23432
+ "interpolate",
23433
+ ["linear"],
23434
+ ["zoom"],
23435
+ 5,
23436
+ 20,
23437
+ 9,
23438
+ 14,
23439
+ 11,
23440
+ 8,
23441
+ 13,
23442
+ 2
23443
+ ]
23444
+ }, tierExpr)
23343
23445
  };
23344
23446
  }
23345
23447
  case "LAKARE": {
@@ -23360,7 +23462,7 @@ function getLabelConfig(layerName, opts = {}) {
23360
23462
  textOffset: [0, 0],
23361
23463
  textFont: ["Open Sans Italic"],
23362
23464
  minzoom: 7,
23363
- layout: orientationLabelLayout()
23465
+ layout: orientationLabelLayout({}, tierExpr)
23364
23466
  };
23365
23467
  }
23366
23468
  case "RIVERS": {
@@ -23381,7 +23483,7 @@ function getLabelConfig(layerName, opts = {}) {
23381
23483
  textOffset: [0, 0],
23382
23484
  textFont: ["Open Sans Italic"],
23383
23485
  minzoom: 7,
23384
- layout: orientationLabelLayout()
23486
+ layout: orientationLabelLayout({}, tierExpr)
23385
23487
  };
23386
23488
  }
23387
23489
  case "CANALS":
@@ -23412,21 +23514,31 @@ function getLabelConfig(layerName, opts = {}) {
23412
23514
  return {
23413
23515
  type: "symbol",
23414
23516
  textField: ["get", "OBJNAM"],
23517
+ // Tier 2 is the SMALL island — the one the owner cannot identify.
23518
+ // Its old curve peaked at 10 px and fell to 10 px, i.e. it never got
23519
+ // its own scale: a name that only exists because you zoomed in was
23520
+ // rendered at the size used for ambient context. It now grows through
23521
+ // harbour zoom (10 → 11.5) while tier 1, the major island already
23522
+ // legible from coastal zoom, is left almost where it was. Small
23523
+ // absolute numbers on purpose: the ask was "clear", not "large" —
23524
+ // the legibility comes mostly from LAND_HALO_WIDTH and the opacity
23525
+ // scale, both of which cost no extra footprint.
23415
23526
  textSize: tieredSizeCurve(tierExpr, [
23416
23527
  [5, [8, 0]],
23417
23528
  [7, [10, 0]],
23418
23529
  [9, [11, 0]],
23419
- [11, [11, 9]],
23420
- [13, [10, 10]],
23421
- [16, [9, 10]]
23530
+ [11, [11.5, 10]],
23531
+ [13, [11.5, 11.5]],
23532
+ [16, [10.5, 11.5]]
23422
23533
  ]),
23423
23534
  textColor: DARK_BG,
23424
23535
  textHaloColor: COLOR.LAND_NATURAL,
23425
- textHaloWidth: ORIENTATION_HALO_WIDTH,
23536
+ // Land halo, not the shared taper — see LAND_HALO_WIDTH.
23537
+ textHaloWidth: LAND_HALO_WIDTH,
23426
23538
  textOffset: [0, 0],
23427
23539
  textFont: ["Open Sans Regular"],
23428
23540
  minzoom: 5,
23429
- layout: orientationLabelLayout()
23541
+ layout: orientationLabelLayout({}, tierExpr)
23430
23542
  };
23431
23543
  }
23432
23544
  case "BUAARE": {
@@ -23451,7 +23563,7 @@ function getLabelConfig(layerName, opts = {}) {
23451
23563
  minzoom: 3,
23452
23564
  layout: orientationLabelLayout({
23453
23565
  "text-letter-spacing": 0.05
23454
- })
23566
+ }, tierExpr)
23455
23567
  };
23456
23568
  }
23457
23569
  case "BRIDGE":
@@ -23543,7 +23655,7 @@ function getLabelConfig(layerName, opts = {}) {
23543
23655
  textOffset: [0, 0],
23544
23656
  textFont: ["Open Sans Regular"],
23545
23657
  minzoom: 7,
23546
- layout: orientationLabelLayout()
23658
+ layout: orientationLabelLayout({}, tierExpr)
23547
23659
  };
23548
23660
  }
23549
23661
  case "SBDARE":
@@ -28381,8 +28493,7 @@ function worldMinusBboxPolygon(bbox) {
28381
28493
 
28382
28494
  // src/atlas/land-layers.ts
28383
28495
  var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
28384
- var RELIEF_MINZOOM2 = 10;
28385
- var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id.startsWith("chrtland-fade-");
28496
+ var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id.startsWith("chrtland-fade-");
28386
28497
  var FADE_PX = 80;
28387
28498
  var FADE_BANDS = 16;
28388
28499
  function fadeAlpha(r) {
@@ -28501,10 +28612,11 @@ function landLayers(opts = {}) {
28501
28612
  }));
28502
28613
  if (opts.relief) {
28503
28614
  layers.push(L({
28615
+ // No minzoom here: LAND_SURFACE_LAYERS floors it with the rest of the
28616
+ // surface, so the two can never drift apart again.
28504
28617
  id: "chrtland-hillshade",
28505
28618
  type: "hillshade",
28506
28619
  source: demSrc,
28507
- minzoom: RELIEF_MINZOOM2,
28508
28620
  paint: {
28509
28621
  "hillshade-exaggeration": T.exaggeration,
28510
28622
  "hillshade-shadow-color": T.shadow,
@@ -28697,7 +28809,19 @@ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
28697
28809
  var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
28698
28810
  var BASE_THEMES = {
28699
28811
  DAY: {
28700
- earth: "#2A3439",
28812
+ // ── `earth` is the jurisdiction tan DIMMED (~0.55), not a blue-grey ──
28813
+ // Outside the jurisdiction the map still owes the reader "what is land".
28814
+ // State 3 in the header is "the geography, under a crosshatch", and until
28815
+ // 2026-08-31 that geography was unreadable: earth #2A3439 against sea
28816
+ // #1B2328 measured 1.25:1, so Hispaniola showed charted boxes with tan
28817
+ // land inside and nothing recognisable around them (owner report: "no
28818
+ // land painting down near the DR at some zooms"). Land is warm and sea is
28819
+ // cool because that is what land and sea are; dimming the JURISDICTION
28820
+ // tan rather than inventing a colour says the true thing — this is the
28821
+ // same land, we simply do not carry charts for it. Held against BOTH
28822
+ // neighbours: legible over sea, and never mistakable for the full tan of
28823
+ // a country we do carry. DAY 3.59:1 vs sea, 3.17:1 vs tan.
28824
+ earth: "#807761",
28701
28825
  water: "#1B2328",
28702
28826
  river: "#24313A",
28703
28827
  boundary: "#4A575E",
@@ -28720,7 +28844,8 @@ var BASE_THEMES = {
28720
28844
  inlandHalo: "#B9CFDD"
28721
28845
  },
28722
28846
  DUSK: {
28723
- earth: "#232B30",
28847
+ // Same rule, this palette's tan: 3.03:1 vs sea, 2.97:1 vs tan.
28848
+ earth: "#6F6752",
28724
28849
  water: "#161D21",
28725
28850
  river: "#1E2930",
28726
28851
  boundary: "#3E4A50",
@@ -28734,7 +28859,10 @@ var BASE_THEMES = {
28734
28859
  inlandHalo: "#93A9B7"
28735
28860
  },
28736
28861
  NIGHT: {
28737
- earth: "#1A2125",
28862
+ // Same rule. NIGHT is inherently compressed — every tone is dark — so it
28863
+ // reaches 2.06:1 vs sea / 2.44:1 vs tan rather than 3:1. Still 1.8x the
28864
+ // separation it had, and pushing further would collide with the tan.
28865
+ earth: "#4F4A3B",
28738
28866
  water: "#10161A",
28739
28867
  river: "#182228",
28740
28868
  boundary: "#333E44",
@@ -28881,12 +29009,38 @@ function baseInlandWaterLayers(opts = {}) {
28881
29009
  const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
28882
29010
  const src = opts.sourceId ?? BASE_REGION;
28883
29011
  const L = (o) => o;
29012
+ const NE_TIER = ["!", ["has", "kind"]];
29013
+ const PLANET_TIER = ["has", "kind"];
29014
+ const HANDOVER = 9;
28884
29015
  return [
29016
+ // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29017
+ //
29018
+ // Both tiers share `inland_water` (tile-join concatenates the same layer
29019
+ // name), and an unsplit fill draws BOTH — which means the NE polygon is
29020
+ // painted over the planet one at every zoom. That is not a cosmetic
29021
+ // double-draw: NE 10m carries no interior rings, so its blob has no
29022
+ // ISLANDS in it, and painting it over the planet's holed polygons buries
29023
+ // every island in the lake under "uncharted water". Owner-reported at
29024
+ // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29025
+ // 'no chart here' boundary"), and measured: rendering the planet tier
29026
+ // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29027
+ // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29028
+ // blob whose edges bear no relation to the shoreline drawn beneath it.
29029
+ //
29030
+ // So each tier owns its own zoom range, the same handover the shoreline
29031
+ // below uses. Known residue, measured over the whole archive: of the 141
29032
+ // NE lakes we still carry, 138 have planet water beneath them; the
29033
+ // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29034
+ // Lake Michigan sliver, which lose their rough fill above z9 because the
29035
+ // planet has no `water` polygon there at all (checked against the raw
29036
+ // pre-clip NDJSON — not a clip artefact).
28885
29037
  L({
28886
29038
  id: "chrtbase-inland-water",
28887
29039
  type: "fill",
28888
29040
  source: src,
28889
29041
  "source-layer": "inland_water",
29042
+ filter: NE_TIER,
29043
+ maxzoom: HANDOVER,
28890
29044
  paint: { "fill-color": T.inlandWater }
28891
29045
  }),
28892
29046
  L({
@@ -28894,6 +29048,26 @@ function baseInlandWaterLayers(opts = {}) {
28894
29048
  type: "fill",
28895
29049
  source: src,
28896
29050
  "source-layer": "inland_water",
29051
+ filter: NE_TIER,
29052
+ maxzoom: HANDOVER,
29053
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29054
+ }),
29055
+ L({
29056
+ id: "chrtbase-inland-water-detail",
29057
+ type: "fill",
29058
+ source: src,
29059
+ "source-layer": "inland_water",
29060
+ filter: PLANET_TIER,
29061
+ minzoom: HANDOVER,
29062
+ paint: { "fill-color": T.inlandWater }
29063
+ }),
29064
+ L({
29065
+ id: "chrtbase-inland-hatch-detail",
29066
+ type: "fill",
29067
+ source: src,
29068
+ "source-layer": "inland_water",
29069
+ filter: PLANET_TIER,
29070
+ minzoom: HANDOVER,
28897
29071
  paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
28898
29072
  }),
28899
29073
  // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
@@ -28913,13 +29087,47 @@ function baseInlandWaterLayers(opts = {}) {
28913
29087
  type: "line",
28914
29088
  source: src,
28915
29089
  "source-layer": "inland_water",
28916
- filter: ["!", ["has", "kind"]],
29090
+ filter: NE_TIER,
29091
+ // Hands the shoreline over to the planet tier at ITS floor. Both
29092
+ // tiers live in `inland_water` from z9 up (tile-join concatenates the
29093
+ // same layer name), and their FILLS merge invisibly — but two
29094
+ // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
29095
+ // outline ran straight across the middle of the lake the planet had
29096
+ // drawn correctly around. The coarse line is right only while it is
29097
+ // the only shape we have.
29098
+ maxzoom: HANDOVER,
28917
29099
  paint: {
28918
29100
  "line-color": T.inlandShore,
28919
29101
  "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
28920
29102
  "line-opacity": 0.9
28921
29103
  }
28922
29104
  }),
29105
+ // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
29106
+ // lakes render with no outline"). It reads its own `inland_shore` layer,
29107
+ // NOT `inland_water`, and that indirection is the whole fix: the mesh
29108
+ // that forced the `!has kind` fence above is a CLIP artefact — an MVT
29109
+ // feature arrives already cut to its own z10 tile, so stroking the
29110
+ // polygon strokes every tile boundary it crosses. The build now walks
29111
+ // those rings, drops the axis-aligned runs that sit on the read zoom's
29112
+ // tile grid, and emits what is left as lines (measured on one New
29113
+ // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
29114
+ // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
29115
+ //
29116
+ // Thinner and quieter than the NE line: this is finer geometry describing
29117
+ // water nobody has surveyed, and approximate data should look
29118
+ // approximate.
29119
+ L({
29120
+ id: "chrtbase-inland-shore-detail",
29121
+ type: "line",
29122
+ source: src,
29123
+ "source-layer": "inland_shore",
29124
+ minzoom: HANDOVER,
29125
+ paint: {
29126
+ "line-color": T.inlandShore,
29127
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
29128
+ "line-opacity": 0.85
29129
+ }
29130
+ }),
28923
29131
  // The lake's name (owner ask 2026-08-28) — the second scoped exception
28924
29132
  // to no-names-on-the-basemap, after the states: WATER-BODY names only.
28925
29133
  // No maxzoom: an uncharted lake keeps its name at every zoom; over
@@ -28936,6 +29144,47 @@ function baseInlandWaterLayers(opts = {}) {
28936
29144
  // deterministic anchor GRID into big lakes — the paper-chart convention
28937
29145
  // of repeating a name along a large water body — and this one layer
28938
29146
  // renders however many anchors are in view; collision culls the rest.
29147
+ // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
29148
+ // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
29149
+ // symbols topmost-layer-first, so the big NE lake wins collision against
29150
+ // the pond beside it, and both lose to the chart's own labels (every ENC
29151
+ // class is emitted after this pair).
29152
+ //
29153
+ // The size threshold is OSM's own `min_zoom`, carried through the build:
29154
+ // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
29155
+ // the filter until the user is close enough for it to be the answer to
29156
+ // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
29157
+ // levels and removes the feature from placement entirely — verified
29158
+ // against maplibre-gl-native rather than assumed, because a text-size-0
29159
+ // gate would still claim a collision slot.
29160
+ //
29161
+ // The tiers are separate LAYERS, not a filtered split of one — see the
29162
+ // builder for why: an archive that a not-yet-deployed style would
29163
+ // mis-render cannot be promoted first, and this one has to be.
29164
+ L({
29165
+ id: "chrtbase-inland-label-detail",
29166
+ type: "symbol",
29167
+ source: src,
29168
+ "source-layer": "inland_water_label_detail",
29169
+ minzoom: HANDOVER,
29170
+ filter: [">=", ["zoom"], ["get", "min_zoom"]],
29171
+ layout: {
29172
+ "text-field": ["get", "name"],
29173
+ "text-font": ["Open Sans Regular"],
29174
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
29175
+ "text-max-width": 8,
29176
+ "text-allow-overlap": false,
29177
+ "text-padding": 6,
29178
+ // Bigger water first within the tier — min_zoom IS the size rank.
29179
+ "symbol-sort-key": ["get", "min_zoom"]
29180
+ },
29181
+ paint: {
29182
+ "text-color": T.inlandLabel,
29183
+ "text-halo-color": T.inlandHalo,
29184
+ "text-halo-width": 1,
29185
+ "text-opacity": 0.9
29186
+ }
29187
+ }),
28939
29188
  L({
28940
29189
  id: "chrtbase-inland-label",
28941
29190
  type: "symbol",
@@ -29167,14 +29416,21 @@ function makeBandVariants(layer, B) {
29167
29416
  return variants;
29168
29417
  }
29169
29418
  var CELL_BAND_OPACITY_CURVE = {
29170
- "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
29171
- "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
29172
- "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
29173
- "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
29174
- "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
29175
- "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
29419
+ "1": [[3, 0.5], [5, 0.42], [16, 0.34]],
29420
+ "2": [[5, 0.52], [7, 0.44], [16, 0.34]],
29421
+ "3": [[7, 0.55], [9, 0.46], [16, 0.36]],
29422
+ "4": [[9, 0.58], [11, 0.5], [16, 0.42]],
29423
+ "5": [[11, 0.62], [13, 0.52], [18, 0.44]],
29424
+ "6": [[13, 0.75], [16, 0.6], [22, 0.5]]
29425
+ };
29426
+ var ORIENTATION_OPACITY_SCALE = {
29427
+ LNDARE: 1.45,
29428
+ LNDRGN: 1.4,
29429
+ BUAARE: 1.25
29176
29430
  };
29177
- function fanOrientationLabelByCellBand(label, bandFan) {
29431
+ var ORIENTATION_OPACITY_MAX = 0.92;
29432
+ var ORIENTATION_BOOST_MINZOOM = 11;
29433
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
29178
29434
  const cellBandExpr = [
29179
29435
  "slice",
29180
29436
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -29182,11 +29438,15 @@ function fanOrientationLabelByCellBand(label, bandFan) {
29182
29438
  3
29183
29439
  ];
29184
29440
  const variants = [];
29441
+ const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
29185
29442
  for (const code of BAND_DRAW_ORDER) {
29186
29443
  const range = BAND_PRIORITY_RANGES[code];
29187
29444
  const curve = CELL_BAND_OPACITY_CURVE[code];
29188
29445
  const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
29189
- for (const [z, o] of curve) opacityExpr.push(z, o);
29446
+ for (const [z, o] of curve) {
29447
+ const s = z >= ORIENTATION_BOOST_MINZOOM ? scale : 1;
29448
+ opacityExpr.push(z, Math.round(Math.min(o * s, ORIENTATION_OPACITY_MAX) * 100) / 100);
29449
+ }
29190
29450
  const cellFiltered = combineWithBandFilter(
29191
29451
  label.filter,
29192
29452
  ["==", cellBandExpr, code]
@@ -29233,6 +29493,33 @@ function fanOrientationLabelByCellBand(label, bandFan) {
29233
29493
  }
29234
29494
  return variants;
29235
29495
  }
29496
+ var MAJOR_WATER_TEXT_PADDING = [
29497
+ "interpolate",
29498
+ ["linear"],
29499
+ ["zoom"],
29500
+ 5,
29501
+ 24,
29502
+ 9,
29503
+ 20,
29504
+ 13,
29505
+ 16,
29506
+ 16,
29507
+ 14
29508
+ ];
29509
+ function splitOrientationFan(fan, hoistedFilter, watermarkFilter, hoistedLayout) {
29510
+ return {
29511
+ hoist: fan.map((l) => ({
29512
+ ...l,
29513
+ id: `${l.id}-major`,
29514
+ filter: combineWithBandFilter(l.filter, hoistedFilter),
29515
+ ...hoistedLayout ? { layout: { ...l.layout ?? {}, ...hoistedLayout } } : {}
29516
+ })),
29517
+ watermark: fan.map((l) => ({
29518
+ ...l,
29519
+ filter: combineWithBandFilter(l.filter, watermarkFilter)
29520
+ }))
29521
+ };
29522
+ }
29236
29523
  function byLabelPriorityBottomUp(bundles) {
29237
29524
  return [...bundles].sort(
29238
29525
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
@@ -29332,6 +29619,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29332
29619
  layers.push(...baseInlandWaterLayers({ palette }));
29333
29620
  }
29334
29621
  const labelLayers = [];
29622
+ const majorWaterLayers = [];
29335
29623
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
29336
29624
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
29337
29625
  const bandFan = opts.bandFan ?? "collapsed";
@@ -29448,19 +29736,18 @@ function generateStyle(palette = "DAY", opts = {}) {
29448
29736
  }
29449
29737
  }
29450
29738
  {
29451
- const TERMINAL_FILL_RE = /^s52-(LNDARE|DEPARE)-fill-band([456])$/;
29452
- for (let i = layers.length - 1; i >= 0; i--) {
29453
- const src = layers[i];
29454
- const m = TERMINAL_FILL_RE.exec(src.id);
29455
- if (!m) continue;
29456
- const clone = {
29457
- ...layers[i],
29458
- id: `s52-${m[1]}-fill-terminal-band${m[2]}`,
29459
- filter: ["all", src.filter, ["==", ["get", "_terminal_fill"], true]]
29460
- };
29461
- delete clone.minzoom;
29462
- clone.maxzoom = src.minzoom ?? 0;
29463
- layers.splice(i + 1, 0, clone);
29739
+ const TERMINAL_FILL_RE = /^s52-(?:LNDARE|DEPARE)-fill-band[456]$/;
29740
+ for (const l of layers) {
29741
+ const lay = l;
29742
+ if (!TERMINAL_FILL_RE.test(lay.id)) continue;
29743
+ const floor = lay.minzoom ?? 0;
29744
+ if (floor <= 0) continue;
29745
+ lay.filter = [
29746
+ "all",
29747
+ lay.filter,
29748
+ ["any", [">=", ["zoom"], floor], ["==", ["get", "_terminal_fill"], true]]
29749
+ ];
29750
+ delete lay.minzoom;
29464
29751
  }
29465
29752
  }
29466
29753
  if (opts.labels === "placename") {
@@ -29469,7 +29756,23 @@ function generateStyle(palette = "DAY", opts = {}) {
29469
29756
  for (const { objectClass, label } of bundles) {
29470
29757
  if (!label) continue;
29471
29758
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
29472
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
29759
+ if (objectClass === "LNDARE" || objectClass === "SEAARE") {
29760
+ const Z = ISLAND_LABEL_HOIST_MINZOOM;
29761
+ const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
29762
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29763
+ [">=", ["zoom"], Z],
29764
+ ["<", ["zoom"], Z]
29765
+ ) : splitOrientationFan(
29766
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29767
+ ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29768
+ [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29769
+ { "text-padding": MAJOR_WATER_TEXT_PADDING }
29770
+ );
29771
+ majorWaterLayers.push(...hoist);
29772
+ labelLayers.push(...watermark);
29773
+ continue;
29774
+ }
29775
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
29473
29776
  }
29474
29777
  }
29475
29778
  }
@@ -29478,7 +29781,12 @@ function generateStyle(palette = "DAY", opts = {}) {
29478
29781
  (b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
29479
29782
  )
29480
29783
  );
29481
- for (const { label } of featureLabelBundles) {
29784
+ let majorsEmitted = majorWaterLayers.length === 0;
29785
+ for (const { label, objectClass: cls } of featureLabelBundles) {
29786
+ if (!majorsEmitted && getLabelPriority(cls) < MAJOR_WATER_LABEL_PRIORITY) {
29787
+ labelLayers.push(...majorWaterLayers);
29788
+ majorsEmitted = true;
29789
+ }
29482
29790
  if (bandFan === "collapsed") {
29483
29791
  labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
29484
29792
  continue;
@@ -29487,6 +29795,7 @@ function generateStyle(palette = "DAY", opts = {}) {
29487
29795
  labelLayers.push(...emitBand(label, code));
29488
29796
  }
29489
29797
  }
29798
+ if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
29490
29799
  layers.push(...labelLayers);
29491
29800
  }
29492
29801
  const symbolSet = opts.symbolSet ?? "s52";