@chrtco/sdk 0.10.18 → 0.10.20

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
@@ -134,7 +134,7 @@ var TileTokenError = class extends Error {
134
134
  this.status = status;
135
135
  }
136
136
  };
137
- var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.mvt(?:\?.*)?$/i;
137
+ var TILE_URL_PATH = /\/v1\/tiles\/[A-Za-z0-9_]+(?:\/[a-z][a-z0-9-]*)?\/\d+\/\d+\/\d+\.(?:mvt|png)(?:\?.*)?$/i;
138
138
  var STYLE_URL_PATH = /\/v1\/styles\/[A-Za-z0-9_]+\/style\.json(?:\?.*)?$/i;
139
139
  var REFRESH_MARGIN_SECONDS = 30;
140
140
  var STALE_GRACE_SECONDS = 1;
@@ -145,6 +145,15 @@ function nowSeconds() {
145
145
  function sleep2(ms) {
146
146
  return new Promise((r) => setTimeout(r, ms));
147
147
  }
148
+ function readBuildTags(raw, region, buildTag) {
149
+ const out = { [region]: buildTag };
150
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
151
+ for (const [key, value] of Object.entries(raw)) {
152
+ if (typeof value === "string" && value) out[key] = value;
153
+ }
154
+ }
155
+ return out;
156
+ }
148
157
  function mapErrorCode(raw, status) {
149
158
  const known = [
150
159
  "invalid_api_key",
@@ -202,6 +211,7 @@ var TileAuth = class {
202
211
  expiresAt: b.expires_at,
203
212
  kid: b.kid,
204
213
  buildTag: b.build_tag,
214
+ buildTags: readBuildTags(b.build_tags, b.region, b.build_tag),
205
215
  region: b.region
206
216
  };
207
217
  }
@@ -277,6 +287,22 @@ var TileAuth = class {
277
287
  if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return null;
278
288
  return entry.response.token;
279
289
  }
290
+ /**
291
+ * Synchronous peek at the cached mint's per-region build ids — `{}` when the
292
+ * cache is cold.
293
+ *
294
+ * `chrt.style()` is synchronous and cannot mint, so this is how a style
295
+ * generated after the cache is warm picks up the build discriminator without
296
+ * the caller threading it by hand. A cold cache yields `{}`, which produces
297
+ * the undiscriminated URL the SDK emitted before this existed — never an
298
+ * error, and never a wrong build.
299
+ */
300
+ peekBuildTags(region) {
301
+ const entry = this.cache.get(region ?? this.defaultRegion);
302
+ if (!entry) return {};
303
+ if (entry.response.expiresAt - nowSeconds() <= STALE_GRACE_SECONDS) return {};
304
+ return entry.response.buildTags;
305
+ }
280
306
  /**
281
307
  * Produce a MapLibre / Mapbox GL `transformRequest`. Attaches
282
308
  * `Authorization: Bearer <jwt>` only to CHRT tile URLs and the CHRT
@@ -330,6 +356,78 @@ var TileAuth = class {
330
356
  }
331
357
  };
332
358
 
359
+ // src/atlas/tile-zoom.ts
360
+ var BUILD_MINZOOM = 0;
361
+ var BUILD_MAXZOOM = 14;
362
+
363
+ // src/atlas/source-registry.ts
364
+ var LAND_REGION = "CHRT_LAND";
365
+ var RELIEF_REGION = "CHRT_RELIEF";
366
+ var BASE_REGION = "CHRT_BASE";
367
+ var BASE_MINZOOM = 0;
368
+ var BASE_MAXZOOM = 11;
369
+ var RELIEF_MINZOOM = 9;
370
+ var RELIEF_MAXZOOM = 12;
371
+ var RELIEF_TILE_SIZE = 512;
372
+ var BUILD_PARAM = "v";
373
+ function tileTemplate(base, region, extension, buildId) {
374
+ const url = `${base}/v1/tiles/${region}/{z}/{x}/{y}.${extension}`;
375
+ const id = buildId?.trim();
376
+ if (!id) return url;
377
+ return `${url}?${BUILD_PARAM}=${encodeURIComponent(id)}`;
378
+ }
379
+ var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
380
+ var DEFAULT_REGION = "NOAA_ENC";
381
+ function buildSources(opts = {}) {
382
+ const mode = opts.mode ?? "pmtiles";
383
+ const region = opts.region ?? DEFAULT_REGION;
384
+ if (mode === "mapbox") {
385
+ return {
386
+ [region]: {
387
+ type: "vector",
388
+ url: `mapbox://chrt.${region.toLowerCase()}`
389
+ }
390
+ };
391
+ }
392
+ const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
393
+ const buildFor = (r) => opts.buildTags?.[r] ?? (r === region ? opts.buildTag : void 0);
394
+ const sources = {
395
+ [region]: {
396
+ type: "vector",
397
+ tiles: [tileTemplate(base, region, "mvt", buildFor(region))],
398
+ minzoom: BUILD_MINZOOM,
399
+ maxzoom: BUILD_MAXZOOM
400
+ }
401
+ };
402
+ if (opts.land) {
403
+ sources[LAND_REGION] = {
404
+ type: "vector",
405
+ tiles: [tileTemplate(base, LAND_REGION, "mvt", buildFor(LAND_REGION))],
406
+ minzoom: BUILD_MINZOOM,
407
+ maxzoom: BUILD_MAXZOOM
408
+ };
409
+ }
410
+ if (opts.base) {
411
+ sources[BASE_REGION] = {
412
+ type: "vector",
413
+ tiles: [`${base}/v1/tiles/${BASE_REGION}/{z}/{x}/{y}.mvt`],
414
+ minzoom: BASE_MINZOOM,
415
+ maxzoom: BASE_MAXZOOM
416
+ };
417
+ }
418
+ if (opts.relief) {
419
+ sources[RELIEF_REGION] = {
420
+ type: "raster-dem",
421
+ tiles: [tileTemplate(base, RELIEF_REGION, "png", buildFor(RELIEF_REGION))],
422
+ encoding: "terrarium",
423
+ tileSize: RELIEF_TILE_SIZE,
424
+ minzoom: RELIEF_MINZOOM,
425
+ maxzoom: RELIEF_MAXZOOM
426
+ };
427
+ }
428
+ return sources;
429
+ }
430
+
333
431
  // src/endpoints/tiles.ts
334
432
  var TilesEndpoint = class {
335
433
  constructor(http, tileAuth, opts) {
@@ -401,13 +499,20 @@ var TilesEndpoint = class {
401
499
  return await res.json();
402
500
  }
403
501
  /**
404
- * Vector tile URL template for the configured region, served by the
405
- * tile-worker at `tiles.chrt.co`. Tiles are JWT-gated — pair this with
502
+ * Vector tile URL for the configured region, served by the tile-worker at
503
+ * `tiles.chrt.co`. Tiles are JWT-gated — pair this with
406
504
  * `chrt.transformRequest()`, which attaches the `Authorization: Bearer`
407
505
  * header. Prefer `chrt.style()`, which wires the source for you.
506
+ *
507
+ * Carries the build discriminator (`?v=`) once the token cache is warm, so
508
+ * two buildTags never share one cache entry — see `BUILD_PARAM`. A cold cache
509
+ * yields the bare URL, which is equally valid: the server ignores the
510
+ * parameter, and warming is a `chrt.warmTiles()` (or one
511
+ * `chrt.transformRequest()` round-trip) away.
408
512
  */
409
513
  tileUrl(z, x, y) {
410
- return `${this.tileBaseUrl}/v1/tiles/${this.region}/${z}/${x}/${y}.mvt`;
514
+ const build = this.tileAuth.peekBuildTags(this.region)[this.region];
515
+ return tileTemplate(this.tileBaseUrl, this.region, "mvt", build).replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
411
516
  }
412
517
  /**
413
518
  * @deprecated Tilesets were consolidated to one PMTiles per region (2026-04);
@@ -633,6 +738,27 @@ var SearchEndpoint = class {
633
738
  }
634
739
  };
635
740
 
741
+ // src/endpoints/poi.ts
742
+ var PoiEndpoint = class {
743
+ constructor(http) {
744
+ this.http = http;
745
+ }
746
+ async list(options) {
747
+ return this.http.request(
748
+ "/poi",
749
+ {
750
+ bbox: options.bbox ? options.bbox.join(",") : void 0,
751
+ near: options.near ? `${options.near[0]},${options.near[1]}` : void 0,
752
+ radius_m: options.radiusM,
753
+ category: options.category,
754
+ limit: options.limit,
755
+ offset: options.offset
756
+ },
757
+ options.signal
758
+ );
759
+ }
760
+ };
761
+
636
762
  // src/endpoints/query.ts
637
763
  var METRES_PER_NAUTICAL_MILE2 = 1852;
638
764
  var QueryEndpoint = class {
@@ -6153,6 +6279,38 @@ var S52_LOOKUP = {
6153
6279
  // polygons that overlap the security-zone CTNARE polygons; with both
6154
6280
  // as flood fills, the harbor inner waters disappeared under stacked
6155
6281
  // magenta. Same boundary-only treatment as CTNARE.
6282
+ // ── DASHED, 2026-08-24. A solid line here read as a second coastline.
6283
+ //
6284
+ // User report: "narragansett bay has 2 offset coastlines... this is so
6285
+ // sloppy. we can't sell this right now." Measured by ablating classes
6286
+ // out of the shipping style and diffing offscreen renders of that exact
6287
+ // frame (z10.54, -71.1745/41.6031): dropping every magenta-painting
6288
+ // class changed 38,580 px and the doubled shoreline vanished; RESARE
6289
+ // ALONE was 37,613 of them — 97.5%. CBLARE, CTNARE, ACHARE and ADMARE
6290
+ // each changed zero.
6291
+ //
6292
+ // Restricted areas in Narragansett Bay follow the shore, so their
6293
+ // boundary runs parallel to the coastline a short distance off it. Drawn
6294
+ // SOLID, in a chart where the coastline is also a solid line, that is a
6295
+ // second coastline to any reader — the geometry is right and the
6296
+ // portrayal lies about what it is.
6297
+ //
6298
+ // This is not a SCAMIN problem, and the first fix attempted was wrong:
6299
+ // the tile pipeline already honours SCAMIN (the z10 tile carries 9
6300
+ // RESARE features, all SCAMIN 1:119,999-1:999,999 — NOAA's own
6301
+ // instruction to draw them at overview scale — while the z11 tile
6302
+ // carries 35). Gating harder would have hidden features the HO asks for.
6303
+ //
6304
+ // Dashed restores the convention this class was already believed to
6305
+ // follow: MIPARE [4,3], TESARE [6,4] and PRDARE [4,2] are all dashed
6306
+ // magenta boundaries, and PRDARE's own comment describes itself as
6307
+ // "matching RESARE/CTNARE/MIPARE/TESARE convention". RESARE was the lone
6308
+ // solid outlier. It also moves toward the S-52 line style for this
6309
+ // class, RESARE51 ("boundary of a restricted area"), a broken magenta
6310
+ // line with inward T-marks — which cannot be used directly yet: the
6311
+ // served sprite (s52-day, 101 entries) carries no RESARE51 pattern, and
6312
+ // a `line-pattern` naming a missing sprite renders NOTHING, which would
6313
+ // turn a portrayal complaint into invisible restricted areas.
6156
6314
  objectClass: "RESARE",
6157
6315
  name: "Restricted Area",
6158
6316
  category: "Regulated Areas",
@@ -6163,7 +6321,8 @@ var S52_LOOKUP = {
6163
6321
  paint: {
6164
6322
  "line-color": "TRFCD",
6165
6323
  "line-width": 1.2,
6166
- "line-opacity": 0.7
6324
+ "line-opacity": 0.7,
6325
+ "line-dasharray": [4, 3]
6167
6326
  }
6168
6327
  },
6169
6328
  viewingGroup: 26040,
@@ -6247,7 +6406,13 @@ var S52_LOOKUP = {
6247
6406
  category: "Regulated Areas",
6248
6407
  displayCategory: "OTHER",
6249
6408
  int1Ref: "N23",
6250
- style: { type: "fill", paint: { "fill-color": "LITYW", "fill-opacity": 0.15 } },
6409
+ // 0.15 painted Hanapepe Bay's dumping ground as a solid land-green wash
6410
+ // over the white deep-water ground — owner-read as "land painting up to
6411
+ // the coastline" (2026-08-31). INT1 N23/N24 portrays a dumping ground
6412
+ // as a dashed limit with a label, not an area tint; 0.04 matches the
6413
+ // TSSLPT/TSSRON faintness so the area reads as WATER with a regulated
6414
+ // overlay, at every tier that shows it.
6415
+ style: { type: "fill", paint: { "fill-color": "LITYW", "fill-opacity": 0.04 } },
6251
6416
  viewingGroup: 26230,
6252
6417
  scamin: 22e3
6253
6418
  },
@@ -7297,12 +7462,19 @@ var S52_LOOKUP = {
7297
7462
  // Inherits RESARE (S-57 112) — boundary-only, no flood fill. The stacked
7298
7463
  // magenta wash that motivated RESARE's treatment applies here too: an
7299
7464
  // inland reach can carry overlapping regulated areas just as a harbour can.
7465
+ // ── DASHED, matching RESARE (2026-08-25). The inland twin was missed
7466
+ // when RESARE was dashed the day before, so USACE inland waters kept
7467
+ // drawing restricted areas as a SOLID magenta line running alongside
7468
+ // the bank — the same "second coastline" a user reported on the coastal
7469
+ // chart, still shipping on the inland one. Found by auditing every
7470
+ // lowercase variant against its NOAA parent: of 14, this was the only
7471
+ // pair that diverged.
7300
7472
  objectClass: "resare",
7301
7473
  name: "Restricted Area (Inland)",
7302
7474
  category: "Regulated Areas",
7303
7475
  displayCategory: "STANDARD",
7304
7476
  int1Ref: "N2",
7305
- style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7 } },
7477
+ style: { type: "line", paint: { "line-color": "TRFCD", "line-width": 1.2, "line-opacity": 0.7, "line-dasharray": [4, 3] } },
7306
7478
  viewingGroup: 26010,
7307
7479
  scamin: 9e4
7308
7480
  },
@@ -22657,14 +22829,19 @@ var ORIENTATION_BAND_SORT_KEY = [
22657
22829
  ["slice", ["coalesce", ["get", "_cell_id"], "??9???"], 2, 3],
22658
22830
  9
22659
22831
  ];
22660
- 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) {
22661
22838
  return {
22662
22839
  "text-anchor": "center",
22663
22840
  "text-allow-overlap": false,
22664
22841
  "text-ignore-placement": false,
22665
22842
  "text-optional": true,
22666
22843
  "text-padding": 15,
22667
- "symbol-sort-key": ORIENTATION_BAND_SORT_KEY,
22844
+ "symbol-sort-key": orientationSortKey(tier),
22668
22845
  ...extras
22669
22846
  };
22670
22847
  }
@@ -22674,15 +22851,17 @@ function tieredSizeCurve(tierExpr, stops) {
22674
22851
  if (i > 0) {
22675
22852
  const [pz, pv] = stops[i - 1];
22676
22853
  const [nz, nv] = stops[i];
22677
- 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)) {
22678
22855
  const hz = Math.round((nz - 0.2) * 10) / 10;
22679
22856
  if (hz > pz) {
22680
22857
  const f = (hz - pz) / (nz - pz);
22681
22858
  withHolds.push([
22682
22859
  hz,
22683
- pv.map(
22684
- (v, t) => v === 0 && nv[t] > 0 ? 0 : Math.round((v + (nv[t] - v) * f) * 10) / 10
22685
- )
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
+ })
22686
22865
  ]);
22687
22866
  }
22688
22867
  }
@@ -22711,6 +22890,19 @@ var ORIENTATION_HALO_WIDTH = [
22711
22890
  16,
22712
22891
  0.7
22713
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
+ ];
22714
22906
  var LABEL_BEHAVIOR = {
22715
22907
  // Orientation labels — render once across all bands.
22716
22908
  SEAARE: "orientation-cross-band",
@@ -22784,6 +22976,9 @@ var LABEL_PRIORITY = {
22784
22976
  };
22785
22977
  var DEFAULT_LABEL_PRIORITY = 450;
22786
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;
22787
22982
  function getLabelPriority(layerName) {
22788
22983
  if (getLabelBehavior(layerName) === "orientation-cross-band") {
22789
22984
  return ORIENTATION_LABEL_PRIORITY;
@@ -22956,21 +23151,25 @@ function getLabelConfig(layerName, opts = {}) {
22956
23151
  // LNDRGN labels until coastal zoom. The Carto basemap already labels
22957
23152
  // major land regions at z5-z6. Columns: t1 major region, t2 local
22958
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.
22959
23157
  textSize: tieredSizeCurve(tierExpr, [
22960
23158
  [5, [0, 0]],
22961
23159
  [7, [11, 0]],
22962
23160
  [9, [12, 0]],
22963
- [11, [12, 9]],
22964
- [13, [11, 11]],
22965
- [16, [10, 10]]
23161
+ [11, [12, 10]],
23162
+ [13, [11.5, 11.5]],
23163
+ [16, [10.5, 11.5]]
22966
23164
  ]),
22967
23165
  textColor: DARK_BG,
22968
23166
  textHaloColor: COLOR.LAND_REGION,
22969
- textHaloWidth: ORIENTATION_HALO_WIDTH,
23167
+ // Land halo, not the shared taper — see LAND_HALO_WIDTH.
23168
+ textHaloWidth: LAND_HALO_WIDTH,
22970
23169
  textOffset: [0, 0],
22971
23170
  textFont: ["Open Sans Regular"],
22972
23171
  minzoom: 5,
22973
- layout: orientationLabelLayout()
23172
+ layout: orientationLabelLayout({}, tierExpr)
22974
23173
  };
22975
23174
  }
22976
23175
  case "LNDMRK":
@@ -23135,14 +23334,56 @@ function getLabelConfig(layerName, opts = {}) {
23135
23334
  // at approach scale (z9) when the user has zoomed in enough that
23136
23335
  // individual banks are useful context. Columns: t1 ocean, t2 sea/gulf,
23137
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.
23138
23379
  textSize: tieredSizeCurve(tierExpr, [
23139
23380
  [3, [14, 8, 0, 0, 0]],
23140
23381
  [5, [16, 13, 9, 0, 0]],
23141
23382
  [7, [14, 14, 12, 0, 0]],
23142
- [9, [12, 13, 14, 0, 9]],
23143
- [11, [10, 12, 13, 0, 11]],
23144
- [13, [9, 11, 12, 10, 11]],
23145
- [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]]
23146
23387
  ]),
23147
23388
  textColor: DARK_BG,
23148
23389
  textHaloColor: COLOR.WATER_DEEP,
@@ -23166,8 +23407,41 @@ function getLabelConfig(layerName, opts = {}) {
23166
23407
  // the tiles and replaces this fan outright — do not restore
23167
23408
  // always-paint on a starvation report; flip `labels: "placename"`
23168
23409
  // instead once the build carries the layer.
23169
- "text-padding": 2
23170
- })
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)
23171
23445
  };
23172
23446
  }
23173
23447
  case "LAKARE": {
@@ -23188,7 +23462,7 @@ function getLabelConfig(layerName, opts = {}) {
23188
23462
  textOffset: [0, 0],
23189
23463
  textFont: ["Open Sans Italic"],
23190
23464
  minzoom: 7,
23191
- layout: orientationLabelLayout()
23465
+ layout: orientationLabelLayout({}, tierExpr)
23192
23466
  };
23193
23467
  }
23194
23468
  case "RIVERS": {
@@ -23209,7 +23483,7 @@ function getLabelConfig(layerName, opts = {}) {
23209
23483
  textOffset: [0, 0],
23210
23484
  textFont: ["Open Sans Italic"],
23211
23485
  minzoom: 7,
23212
- layout: orientationLabelLayout()
23486
+ layout: orientationLabelLayout({}, tierExpr)
23213
23487
  };
23214
23488
  }
23215
23489
  case "CANALS":
@@ -23240,21 +23514,31 @@ function getLabelConfig(layerName, opts = {}) {
23240
23514
  return {
23241
23515
  type: "symbol",
23242
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.
23243
23526
  textSize: tieredSizeCurve(tierExpr, [
23244
23527
  [5, [8, 0]],
23245
23528
  [7, [10, 0]],
23246
23529
  [9, [11, 0]],
23247
- [11, [11, 9]],
23248
- [13, [10, 10]],
23249
- [16, [9, 10]]
23530
+ [11, [11.5, 10]],
23531
+ [13, [11.5, 11.5]],
23532
+ [16, [10.5, 11.5]]
23250
23533
  ]),
23251
23534
  textColor: DARK_BG,
23252
23535
  textHaloColor: COLOR.LAND_NATURAL,
23253
- textHaloWidth: ORIENTATION_HALO_WIDTH,
23536
+ // Land halo, not the shared taper — see LAND_HALO_WIDTH.
23537
+ textHaloWidth: LAND_HALO_WIDTH,
23254
23538
  textOffset: [0, 0],
23255
23539
  textFont: ["Open Sans Regular"],
23256
23540
  minzoom: 5,
23257
- layout: orientationLabelLayout()
23541
+ layout: orientationLabelLayout({}, tierExpr)
23258
23542
  };
23259
23543
  }
23260
23544
  case "BUAARE": {
@@ -23279,7 +23563,7 @@ function getLabelConfig(layerName, opts = {}) {
23279
23563
  minzoom: 3,
23280
23564
  layout: orientationLabelLayout({
23281
23565
  "text-letter-spacing": 0.05
23282
- })
23566
+ }, tierExpr)
23283
23567
  };
23284
23568
  }
23285
23569
  case "BRIDGE":
@@ -23371,7 +23655,7 @@ function getLabelConfig(layerName, opts = {}) {
23371
23655
  textOffset: [0, 0],
23372
23656
  textFont: ["Open Sans Regular"],
23373
23657
  minzoom: 7,
23374
- layout: orientationLabelLayout()
23658
+ layout: orientationLabelLayout({}, tierExpr)
23375
23659
  };
23376
23660
  }
23377
23661
  case "SBDARE":
@@ -25755,7 +26039,7 @@ var S52_POINT_SVGS = {
25755
26039
  "LIGHTS14": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.8438" transform="translate(16 16) scale(0.8438) translate(-16 -16)">\n <path d="M 16.3 28.0 L 11.8 7.1 L 11.8 6.3 L 11.8 4.9 L 12.4 3.5 L 13.2 3.0 L 14.3 2.1 L 16.0 1.9 L 17.4 2.1 L 18.5 2.4 L 19.6 3.2 L 20.2 4.6 L 20.4 5.7 L 20.4 6.9 L 16.3 28.0 Z" fill="CHMGD" />\n <path d="M 16.3 28.0 L 11.8 7.1 L 11.8 6.3 L 11.8 4.9 L 12.4 3.5 L 13.2 3.0 L 14.3 2.1 L 16.0 1.9 L 17.4 2.1 L 18.5 2.4 L 19.6 3.2 L 20.2 4.6 L 20.4 5.7 L 20.4 6.9 L 16.3 28.0" fill="none" stroke="CHMGD" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 21.3 5.2 L 21.0 6.6 L 16.3 31.0" fill="none" stroke="CHBLK" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 28.0 L 11.5 6.6 L 11.8 5.2 L 12.1 4.1 L 12.9 3.2 L 14.1 2.1 L 15.4 1.9 L 16.3 1.9 L 17.4 2.1 L 18.8 2.7 L 19.6 3.5 L 20.2 4.6 L 20.4 5.7 L 20.2 7.7 L 16.3 28.0" fill="none" stroke="CHMGD" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 31.0 L 10.7 6.0 L 11.5 3.5 L 13.8 1.6 L 16.3 1.0 L 18.5 1.9 L 19.9 2.7 L 21.0 4.9" fill="none" stroke="CHBLK" stroke-width="1.185" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
25756
26040
  "LIGHTS81": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.3896" transform="translate(16 16) scale(0.3896) translate(-16 -16)">\n <path d="M 1.0 25.9 L 6.9 5.9 L 12.9 26.0 L 19.0 5.9 L 25.0 26.1 L 31.0 5.9" fill="none" stroke="CHMGD" stroke-width="2.567" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
25757
26041
  "LIGHTS82": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.6083" transform="translate(16 16) scale(0.6083) translate(-16 -15.98)">\n <circle cx="6.96" cy="17.80" r="5.96" fill="none" stroke="CHMGD" stroke-width="3.288" />\n <path d="M 14.9 23.1 L 25.9 23.5" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 13.3 12.1 L 28.0 8.2" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 15.8 17.5 L 31.0 16.0" fill="none" stroke="CHMGD" stroke-width="3.288" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
25758
- "LITDEF11": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.7292" transform="translate(16 16) scale(0.7292) translate(-16 -16)">\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0 Z" fill="CHMGD" />\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 31.0 L 10.9 6.4 L 10.9 4.7 L 11.4 3.1 L 12.3 2.3 L 13.6 1.3 L 15.2 1.0 L 16.3 1.0 L 17.6 1.0 L 18.9 1.5 L 20.0 2.6 L 20.8 4.2 L 21.1 5.3 L 20.8 7.7 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
26042
+ "LITDEF11": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.7292" transform="translate(16 16) scale(0.7292) translate(-16.1 -14.95)">\n <g transform="translate(10 10) rotate(135) scale(0.9) translate(-16.3 -31)">\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0 Z" fill="CHMGD" />\n <path d="M 16.3 31.0 L 10.9 6.9 L 10.9 5.8 L 11.2 4.2 L 12.0 2.8 L 12.8 2.1 L 14.1 1.0 L 16.0 1.0 L 17.6 1.0 L 18.7 1.3 L 20.3 2.3 L 20.8 3.9 L 21.1 5.3 L 21.1 6.6 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.3 31.0 L 10.9 6.4 L 10.9 4.7 L 11.4 3.1 L 12.3 2.3 L 13.6 1.3 L 15.2 1.0 L 16.3 1.0 L 17.6 1.0 L 18.9 1.5 L 20.0 2.6 L 20.8 4.2 L 21.1 5.3 L 20.8 7.7 L 16.3 31.0" fill="none" stroke="CHMGD" stroke-width="1.371" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n </g>\n</svg>\n',
25759
26043
  "LITFLT01": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.9375" transform="translate(16 16) scale(0.9375) translate(-16 -15.985)">\n <circle cx="16.00" cy="18.97" r="2.40" fill="none" stroke="CHBLK" stroke-width="2.133" />\n <path d="M 1.0 19.0 L 12.7 19.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 6.0 19.0 L 2.7 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 26.0 19.0 L 29.3 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 11.0 14.0 L 12.7 10.6 L 19.3 10.6 L 21.0 14.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 2.7 12.3 L 9.3 14.0 L 22.7 14.0 L 29.3 12.3" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 19.3 19.0 L 31.0 19.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
25760
26044
  "LITFLT02": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.6385" transform="translate(16 16) scale(0.6385) translate(-16 -16)">\n <path d="M 1.0 13.0 L 5.6 21.5 L 26.9 21.5 L 31.0 12.9 Z" fill="CHBLK" />\n <path d="M 1.0 13.0 L 5.6 21.5 L 26.9 21.5 L 31.0 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 1.0 12.9 L 5.6 21.5 L 26.9 21.5 L 30.9 12.9 L 1.0 12.9 Z" fill="CHBLK" />\n <path d="M 1.0 12.9 L 5.6 21.5 L 26.9 21.5 L 30.9 12.9 L 1.0 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 10.9 12.9 L 10.9 10.5 L 21.2 10.5 L 21.2 12.9" fill="none" stroke="CHBLK" stroke-width="1.566" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
25761
26045
  "LITVES01": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">\n <g data-chrt-scale="0.9375" transform="translate(16 16) scale(0.9375) translate(-16 -15.98)">\n <circle cx="16.00" cy="24.83" r="2.33" fill="none" stroke="CHBLK" stroke-width="2.133" />\n <path d="M 1.0 24.8 L 12.7 24.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 6.0 24.8 L 2.7 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 26.0 24.8 L 29.3 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.0 19.0 L 16.0 9.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 9.3 19.0 L 9.3 13.0" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 22.7 19.0 L 22.7 13.2" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 16.0 9.8 L 16.0 4.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 12.7 6.5 L 19.3 9.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 12.7 9.8 L 19.3 6.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 4.7 21.5 L 10.2 21.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 21.8 21.5 L 27.3 21.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 2.7 16.5 L 9.2 19.0 L 16.0 19.0 L 22.7 19.0 L 29.3 16.5" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n <path d="M 19.3 24.8 L 31.0 24.8" fill="none" stroke="CHBLK" stroke-width="2.133" stroke-linecap="butt" stroke-linejoin="round" />\n </g>\n</svg>\n',
@@ -26270,6 +26554,8 @@ var CHRT_GLYPH_SVGS = {
26270
26554
  "cardinal-w": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 6 12 1.5h8Z" fill="#101418"/><path d="M16 6.5 20 11h-8Z" fill="#101418"/><path d="M13 12h6l1 13H12Z" fill="#FFC805"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
26271
26555
  "daymark-square": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="6" y="6" width="20" height="20" rx="1.5" fill="#fff"/></svg>',
26272
26556
  "daymark-triangle": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M16 5 28 26H4Z" fill="#fff"/></svg>',
26557
+ "hatch-nocoverage": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#8A97A0" stroke-width="0.8" fill="none"/><path d="M-3 15 L15 33 M-3 3 L21 27 M3 -3 L27 21 M15 -3 L27 9" stroke="#8A97A0" stroke-width="0.8" fill="none"/></svg>',
26558
+ "hatch-uncharted": '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M-3 9 L9 -3 M-3 21 L21 -3 M3 27 L27 3 M15 27 L27 15" stroke="#5B7383" stroke-width="0.65" fill="none"/></svg>',
26273
26559
  "isolated-danger": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="3.5" r="2.4" fill="#101418"/><circle cx="16" cy="9.5" r="2.4" fill="#101418"/><path d="M13 13h6l1 12H12Z" fill="#101418"/><rect x="12.4" y="17.5" width="7.2" height="3.4" fill="#E5484D"/><path d="M6 28h20" stroke="#101418" stroke-width="2.4" stroke-linecap="round"/></svg>',
26274
26560
  "light-flare": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="11" cy="11" r="3.2" fill="#fff"/><path d="M13.4 13.4 24 19.5a2.6 2.6 0 0 1-4.5 4.5L13.4 13.4Z" fill="#fff"/></svg>',
26275
26561
  "lightship": '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M6 20l3-5h14l3 5-3.5 4h-13Z" fill="#fff"/><path d="M16 15V8" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/><circle cx="16" cy="6" r="2.2" fill="#fff"/><path d="M5 28h22" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>',
@@ -26718,59 +27004,6 @@ function iconExpressionForFamily(objectClass, family) {
26718
27004
  }
26719
27005
  }
26720
27006
 
26721
- // src/atlas/tile-zoom.ts
26722
- var BUILD_MINZOOM = 0;
26723
- var BUILD_MAXZOOM = 14;
26724
-
26725
- // src/atlas/source-registry.ts
26726
- var LAND_REGION = "CHRT_LAND";
26727
- var RELIEF_REGION = "CHRT_RELIEF";
26728
- var RELIEF_MINZOOM = 8;
26729
- var RELIEF_MAXZOOM = 13;
26730
- var RELIEF_TILE_SIZE = 512;
26731
- var DEFAULT_TILE_BASE_URL2 = "https://tiles.chrt.co";
26732
- var DEFAULT_REGION = "NOAA_ENC";
26733
- function buildSources(opts = {}) {
26734
- const mode = opts.mode ?? "pmtiles";
26735
- const region = opts.region ?? DEFAULT_REGION;
26736
- if (mode === "mapbox") {
26737
- return {
26738
- [region]: {
26739
- type: "vector",
26740
- url: `mapbox://chrt.${region.toLowerCase()}`
26741
- }
26742
- };
26743
- }
26744
- const base = (opts.tileBaseUrl ?? DEFAULT_TILE_BASE_URL2).replace(/\/+$/, "");
26745
- const sources = {
26746
- [region]: {
26747
- type: "vector",
26748
- tiles: [`${base}/v1/tiles/${region}/{z}/{x}/{y}.mvt`],
26749
- minzoom: BUILD_MINZOOM,
26750
- maxzoom: BUILD_MAXZOOM
26751
- }
26752
- };
26753
- if (opts.land) {
26754
- sources[LAND_REGION] = {
26755
- type: "vector",
26756
- tiles: [`${base}/v1/tiles/${LAND_REGION}/{z}/{x}/{y}.mvt`],
26757
- minzoom: BUILD_MINZOOM,
26758
- maxzoom: BUILD_MAXZOOM
26759
- };
26760
- }
26761
- if (opts.relief) {
26762
- sources[RELIEF_REGION] = {
26763
- type: "raster-dem",
26764
- tiles: [`${base}/v1/tiles/${RELIEF_REGION}/{z}/{x}/{y}.png`],
26765
- encoding: "terrarium",
26766
- tileSize: RELIEF_TILE_SIZE,
26767
- minzoom: RELIEF_MINZOOM,
26768
- maxzoom: RELIEF_MAXZOOM
26769
- };
26770
- }
26771
- return sources;
26772
- }
26773
-
26774
27007
  // src/atlas/layer-builder.ts
26775
27008
  var SCAMIN_DECLUTTER_CLASSES = /* @__PURE__ */ new Set([
26776
27009
  "OFSPLF",
@@ -27098,7 +27331,27 @@ function buildLayersForClass(objectClass, ct, sourceId = DEFAULT_REGION, opts =
27098
27331
  layer.layout = {
27099
27332
  ...layer.layout,
27100
27333
  "icon-allow-overlap": true,
27101
- "icon-ignore-placement": true
27334
+ "icon-ignore-placement": true,
27335
+ // Flare-specific size (owner ask 2026-08-31: "make the flares a
27336
+ // little smaller — they should scale down a lot as you zoom out").
27337
+ // The safety-critical curve serves buoy/beacon HULLS, whose set
27338
+ // this overrides for LIGHTS only: flares are annotations on a
27339
+ // point, not shapes a mariner ranges against, so they start small
27340
+ // at planning scales and only approach full size at berthing
27341
+ // zooms. Applied after the safety-critical spread so it wins.
27342
+ "icon-size": [
27343
+ "interpolate",
27344
+ ["linear"],
27345
+ ["zoom"],
27346
+ 6,
27347
+ 0.3,
27348
+ 12,
27349
+ 0.42,
27350
+ 14,
27351
+ 0.6,
27352
+ 18,
27353
+ 1
27354
+ ]
27102
27355
  };
27103
27356
  const valnmrNum = ["to-number", ["coalesce", ["get", "VALNMR"], ["get", "valnmr"], 0], 0];
27104
27357
  const isMajor = [">=", valnmrNum, 10];
@@ -28063,85 +28316,265 @@ function getOrderedClasses() {
28063
28316
  return ordered;
28064
28317
  }
28065
28318
 
28066
- // src/atlas/land-layers.ts
28067
- var FADE_PX = 80;
28068
- var FADE_BANDS = 16;
28069
- function fadeAlpha(r) {
28070
- return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
28071
- }
28072
- var LAND_THEMES = {
28073
- DAY: {
28074
- base: "#e8d9b0",
28075
- cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
28076
- contour: "#a89878",
28077
- contourIndex: "#8a7a5c",
28078
- road: "#d9c9a4",
28079
- roadMajor: "#d2bd8e",
28080
- label: "#5b4f3a",
28081
- halo: "#f2ead6",
28082
- shadow: "#6b5a3e",
28083
- highlight: "#fffaf0",
28084
- fade: "#e8f2f8",
28085
- // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
28086
- // hillshade-style.mjs warns "starts competing with depth areas" — a
28087
- // deliberate, sighted choice made against rendered evidence, not a
28088
- // default. If soundings or DEPARE ever read as fighting the terrain,
28089
- // this number is the first thing to walk back.
28090
- exaggeration: 0.8
28091
- },
28092
- DUSK: {
28093
- base: "#4a4433",
28094
- cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
28095
- contour: "#6b6250",
28096
- contourIndex: "#837961",
28097
- road: "#565040",
28098
- roadMajor: "#63594440",
28099
- label: "#c3b898",
28100
- halo: "#2b2820",
28101
- shadow: "#2e2a20",
28102
- highlight: "#8a8068",
28103
- fade: "#16202a",
28104
- // Unchanged. DUSK was not the palette the 0.8 decision was made against.
28105
- exaggeration: 0.45
28106
- },
28107
- NIGHT: {
28108
- base: "#2a2620",
28109
- cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
28110
- contour: "#413b30",
28111
- contourIndex: "#524a3b",
28112
- road: "#332f27",
28113
- roadMajor: "#3d382d",
28114
- label: "#7d745e",
28115
- halo: "#15130f",
28116
- shadow: "#1a1712",
28117
- highlight: "#4a4436",
28118
- fade: "#07090b",
28119
- // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
28120
- // under red night vision is a grey haze over exactly the contrast a
28121
- // skipper is navigating by. Left at the shipped value rather than
28122
- // lowered, because lowering it is a separate decision from this one.
28123
- exaggeration: 0.45
28124
- }
28319
+ // src/atlas/band-priority.ts
28320
+ var BAND_PRIORITY_RANGES = {
28321
+ "1": { minzoom: 0 },
28322
+ "2": { minzoom: 5 },
28323
+ "3": { minzoom: 7 },
28324
+ "4": { minzoom: 9 },
28325
+ "5": { minzoom: 11 },
28326
+ "6": { minzoom: 11 }
28125
28327
  };
28126
- var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
28127
- var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
28128
- var anchorOffset = (em) => [
28129
- "match",
28130
- ["get", "anchor"],
28131
- "left",
28132
- ["literal", [0.6, 0]],
28133
- "right",
28134
- ["literal", [-0.6, 0]],
28135
- "bottom",
28136
- ["literal", [0, -em]],
28137
- ["literal", [0, em]]
28138
- ];
28139
- var FONT = ["Open Sans Regular"];
28140
- var FONT_ITALIC = ["Open Sans Italic"];
28141
- function landLayers(opts = {}) {
28142
- const palette = opts.palette ?? "DAY";
28143
- const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
28144
- const src = opts.sourceId ?? LAND_REGION;
28328
+ var BAND_DATA_MINZOOM = {
28329
+ "1": 0,
28330
+ "2": 5,
28331
+ "3": 7,
28332
+ "4": 9,
28333
+ "5": 11,
28334
+ "6": 13
28335
+ };
28336
+ function dominatedVariantMaxzoom(D) {
28337
+ return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
28338
+ }
28339
+ var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
28340
+ "LNDARE",
28341
+ "DEPARE",
28342
+ "UNSARE",
28343
+ "LAKARE",
28344
+ "DRGARE",
28345
+ "SEAARE"
28346
+ ]);
28347
+ function isExemptOpaqueFill(layer) {
28348
+ if (layer.type !== "fill") return false;
28349
+ for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
28350
+ if (layer.id.startsWith(`s52-${cls}-`)) return true;
28351
+ }
28352
+ return false;
28353
+ }
28354
+ var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
28355
+ "1",
28356
+ "2",
28357
+ "3"
28358
+ ]);
28359
+ function bandVariantMinzoom(layer, B) {
28360
+ const classMinzoom = layer.minzoom ?? 0;
28361
+ if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
28362
+ return classMinzoom;
28363
+ }
28364
+ return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
28365
+ }
28366
+ var GENERALISED_AREA_FILL_CLASSES = /* @__PURE__ */ new Set([
28367
+ "BUAARE",
28368
+ // 2026-08-28, New York Harbor: the "New York Marine Inspection Zone and
28369
+ // Captain of the Port Zone" RESARE exists at bands 2/3 as a generalized
28370
+ // shore-hugging polygon whose point-sampled dominance tag MISSED (the
28371
+ // Providence-BUAARE failure mode), so its magenta dashed boundary drew
28372
+ // OFFSET over the band-5 detail at z11.5 — read in the field as "a
28373
+ // magenta shoreline bleeding down from a less coarse band". The same
28374
+ // tile carried untagged band-2 ADMARE and CTNARE giants in the same
28375
+ // state. All three are area classes whose coarse-band boundaries hug
28376
+ // shorelines and whose fine-band copies are authoritative.
28377
+ "RESARE",
28378
+ "CTNARE",
28379
+ "ADMARE"
28380
+ ]);
28381
+ var GENERALISED_AREA_MAX_BAND = 3;
28382
+ function generalisedAreaCeiling(layer, B) {
28383
+ if (layer.type !== "fill" && layer.type !== "line") return null;
28384
+ if (parseInt(B, 10) > GENERALISED_AREA_MAX_BAND) return null;
28385
+ for (const cls of GENERALISED_AREA_FILL_CLASSES) {
28386
+ if (layer.id.startsWith(`s52-${cls}-`)) return BAND_PRIORITY_RANGES["5"].minzoom;
28387
+ }
28388
+ return null;
28389
+ }
28390
+ var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
28391
+ var CHART_BAND_CODES = {
28392
+ Overview: ["1"],
28393
+ General: ["2"],
28394
+ Coastal: ["3"],
28395
+ Approach: ["4"],
28396
+ Harbor: ["5", "6"]
28397
+ };
28398
+ var CHART_CASCADE_CODES = {
28399
+ Overview: ["1"],
28400
+ General: ["1", "2"],
28401
+ Coastal: ["1", "2", "3"],
28402
+ Approach: ["1", "2", "3", "4"],
28403
+ Harbor: ["1", "2", "3", "4", "5", "6"]
28404
+ };
28405
+ var CHRT_STYLE_MODES = [
28406
+ "continuous",
28407
+ ...Object.keys(CHART_BAND_CODES).map(
28408
+ (band) => `chart:${band}`
28409
+ )
28410
+ ];
28411
+ function normalizeStyleMode(raw) {
28412
+ if (raw === null || raw === void 0 || raw === "") return "continuous";
28413
+ const lower = raw.toLowerCase();
28414
+ return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
28415
+ }
28416
+ function styleModeBand(mode) {
28417
+ return mode === "continuous" ? null : mode.slice("chart:".length);
28418
+ }
28419
+ function bandFilterExpression(codes) {
28420
+ const list = Array.isArray(codes) ? codes : [codes];
28421
+ if (list.length === 1) {
28422
+ return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
28423
+ }
28424
+ return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
28425
+ }
28426
+ function combineWithBandFilter(baseFilter, bandFilter) {
28427
+ if (!baseFilter) return bandFilter;
28428
+ if (baseFilter[0] === "all") {
28429
+ return ["all", ...baseFilter.slice(1), bandFilter];
28430
+ }
28431
+ return ["all", baseFilter, bandFilter];
28432
+ }
28433
+ var NOT_DOMINATED_FILTER = [
28434
+ "all",
28435
+ ["!", ["has", "_dominated_by_min"]],
28436
+ ["!=", ["get", "_dominated"], true]
28437
+ ];
28438
+ function dominatedByMinFilter(D) {
28439
+ const dInt = parseInt(D, 10);
28440
+ if (D === "6") {
28441
+ return [
28442
+ "any",
28443
+ ["==", ["get", "_dominated_by_min"], dInt],
28444
+ ["==", ["get", "_dominated"], true]
28445
+ ];
28446
+ }
28447
+ return ["==", ["get", "_dominated_by_min"], dInt];
28448
+ }
28449
+ function withinBboxFilter(bbox) {
28450
+ const [w, s] = bbox.sw;
28451
+ const [e, n] = bbox.ne;
28452
+ return [
28453
+ "within",
28454
+ {
28455
+ type: "Polygon",
28456
+ coordinates: [
28457
+ [
28458
+ [w, s],
28459
+ [e, s],
28460
+ [e, n],
28461
+ [w, n],
28462
+ [w, s]
28463
+ ]
28464
+ ]
28465
+ }
28466
+ ];
28467
+ }
28468
+ function worldMinusBboxPolygon(bbox) {
28469
+ const [w, s] = bbox.sw;
28470
+ const [e, n] = bbox.ne;
28471
+ return {
28472
+ type: "Polygon",
28473
+ coordinates: [
28474
+ [
28475
+ [-180, -85],
28476
+ [180, -85],
28477
+ [180, 85],
28478
+ [-180, 85],
28479
+ [-180, -85]
28480
+ ],
28481
+ // Inner ring (hole) — winding opposite to outer ring per GeoJSON
28482
+ // spec. MapLibre is permissive but we follow strict winding.
28483
+ [
28484
+ [w, s],
28485
+ [w, n],
28486
+ [e, n],
28487
+ [e, s],
28488
+ [w, s]
28489
+ ]
28490
+ ]
28491
+ };
28492
+ }
28493
+
28494
+ // src/atlas/land-layers.ts
28495
+ var LAND_SURFACE_MINZOOM = BAND_PRIORITY_RANGES["5"].minzoom;
28496
+ var LAND_SURFACE_LAYERS = (id) => id === "chrtland-base" || id === "chrtland-cover" || id === "chrtland-contour" || id === "chrtland-hillshade" || id.startsWith("chrtland-fade-");
28497
+ var FADE_PX = 80;
28498
+ var FADE_BANDS = 16;
28499
+ function fadeAlpha(r) {
28500
+ return Math.max(0, Math.min(1, (FADE_PX / 2 - Math.abs(r)) / FADE_PX));
28501
+ }
28502
+ function landBaseColor(palette) {
28503
+ return (LAND_THEMES[palette] ?? LAND_THEMES.DAY).base;
28504
+ }
28505
+ var LAND_THEMES = {
28506
+ DAY: {
28507
+ base: "#e8d9b0",
28508
+ cover: { wood: "#cfd8b4", wetland: "#c9d3c4", urban: "#dedad2", sand: "#efe4c2" },
28509
+ contour: "#a89878",
28510
+ contourIndex: "#8a7a5c",
28511
+ road: "#d9c9a4",
28512
+ roadMajor: "#d2bd8e",
28513
+ label: "#5b4f3a",
28514
+ halo: "#f2ead6",
28515
+ shadow: "#6b5a3e",
28516
+ highlight: "#fffaf0",
28517
+ fade: "#e8f2f8",
28518
+ // Raised from 0.45 on 2026-08-23. ⚠ Above the ~0.5 that
28519
+ // hillshade-style.mjs warns "starts competing with depth areas" — a
28520
+ // deliberate, sighted choice made against rendered evidence, not a
28521
+ // default. If soundings or DEPARE ever read as fighting the terrain,
28522
+ // this number is the first thing to walk back.
28523
+ exaggeration: 0.8
28524
+ },
28525
+ DUSK: {
28526
+ base: "#4a4433",
28527
+ cover: { wood: "#414a38", wetland: "#3f4740", urban: "#4e4a42", sand: "#544c39" },
28528
+ contour: "#6b6250",
28529
+ contourIndex: "#837961",
28530
+ road: "#565040",
28531
+ roadMajor: "#63594440",
28532
+ label: "#c3b898",
28533
+ halo: "#2b2820",
28534
+ shadow: "#2e2a20",
28535
+ highlight: "#8a8068",
28536
+ fade: "#16202a",
28537
+ // Unchanged. DUSK was not the palette the 0.8 decision was made against.
28538
+ exaggeration: 0.45
28539
+ },
28540
+ NIGHT: {
28541
+ base: "#2a2620",
28542
+ cover: { wood: "#242a20", wetland: "#232823", urban: "#2c2925", sand: "#302b22" },
28543
+ contour: "#413b30",
28544
+ contourIndex: "#524a3b",
28545
+ road: "#332f27",
28546
+ roadMajor: "#3d382d",
28547
+ label: "#7d745e",
28548
+ halo: "#15130f",
28549
+ shadow: "#1a1712",
28550
+ highlight: "#4a4436",
28551
+ fade: "#07090b",
28552
+ // ⚠ DELIBERATELY NOT RAISED. See the type's note: a strong hillshade
28553
+ // under red night vision is a grey haze over exactly the contrast a
28554
+ // skipper is navigating by. Left at the shipped value rather than
28555
+ // lowered, because lowering it is a separate decision from this one.
28556
+ exaggeration: 0.45
28557
+ }
28558
+ };
28559
+ var FITS = ["<=", ["coalesce", ["get", "min_z"], 0], ["zoom"]];
28560
+ var ANCHOR = ["coalesce", ["get", "anchor"], "top"];
28561
+ var anchorOffset = (em) => [
28562
+ "match",
28563
+ ["get", "anchor"],
28564
+ "left",
28565
+ ["literal", [0.6, 0]],
28566
+ "right",
28567
+ ["literal", [-0.6, 0]],
28568
+ "bottom",
28569
+ ["literal", [0, -em]],
28570
+ ["literal", [0, em]]
28571
+ ];
28572
+ var FONT = ["Open Sans Regular"];
28573
+ var FONT_ITALIC = ["Open Sans Italic"];
28574
+ function landLayers(opts = {}) {
28575
+ const palette = opts.palette ?? "DAY";
28576
+ const T = LAND_THEMES[palette] ?? LAND_THEMES.DAY;
28577
+ const src = opts.sourceId ?? LAND_REGION;
28145
28578
  const demSrc = opts.reliefSourceId ?? RELIEF_REGION;
28146
28579
  const L = (o) => o;
28147
28580
  const layers = [
@@ -28179,6 +28612,8 @@ function landLayers(opts = {}) {
28179
28612
  }));
28180
28613
  if (opts.relief) {
28181
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.
28182
28617
  id: "chrtland-hillshade",
28183
28618
  type: "hillshade",
28184
28619
  source: demSrc,
@@ -28234,12 +28669,12 @@ function landLayers(opts = {}) {
28234
28669
  }
28235
28670
  }));
28236
28671
  layers.push(L({
28237
- id: "chrtland-town",
28672
+ id: "chrtland-city-major",
28238
28673
  type: "symbol",
28239
28674
  source: src,
28240
28675
  "source-layer": "land_town",
28241
- minzoom: 9,
28242
- filter: FITS,
28676
+ minzoom: 6.5,
28677
+ filter: [">=", ["coalesce", ["get", "prank"], 0], 10],
28243
28678
  layout: {
28244
28679
  "text-field": [
28245
28680
  "case",
@@ -28248,211 +28683,526 @@ function landLayers(opts = {}) {
28248
28683
  ["get", "name"]
28249
28684
  ],
28250
28685
  "text-font": FONT,
28251
- "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
28252
- "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
28686
+ "text-size": [
28687
+ "interpolate",
28688
+ ["linear"],
28689
+ ["zoom"],
28690
+ 6.5,
28691
+ 11.5,
28692
+ 9,
28693
+ ["+", 10, ["*", 0.4, ["coalesce", ["get", "prank"], 10]]],
28694
+ 13,
28695
+ ["+", 13, ["*", 0.5, ["coalesce", ["get", "prank"], 10]]]
28696
+ ],
28697
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0.02],
28253
28698
  "text-offset": anchorOffset(0.75),
28254
28699
  "text-anchor": ANCHOR,
28255
- "text-padding": 6,
28700
+ "text-padding": 10,
28256
28701
  "text-allow-overlap": false,
28257
- "symbol-sort-key": 2
28702
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
28258
28703
  },
28259
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28704
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.6 }
28260
28705
  }));
28261
28706
  layers.push(L({
28262
- id: "chrtland-summit",
28707
+ id: "chrtland-town-port",
28263
28708
  type: "symbol",
28264
28709
  source: src,
28265
- "source-layer": "land_summit",
28266
- minzoom: 11.5,
28267
- filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
28710
+ "source-layer": "land_town",
28711
+ minzoom: 8.5,
28712
+ filter: [
28713
+ "all",
28714
+ ["==", ["get", "enc_dup"], true],
28715
+ ["<", ["coalesce", ["get", "prank"], 0], 10]
28716
+ ],
28268
28717
  layout: {
28269
- "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
28718
+ "text-field": ["get", "name"],
28270
28719
  "text-font": FONT,
28271
- "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
28272
- "text-offset": anchorOffset(0.8),
28720
+ "text-size": ["interpolate", ["linear"], ["zoom"], 8.5, 11, 12, 13.5],
28721
+ "text-offset": anchorOffset(0.75),
28273
28722
  "text-anchor": ANCHOR,
28274
- "text-padding": 5,
28723
+ "text-padding": 8,
28275
28724
  "text-allow-overlap": false,
28276
- "symbol-sort-key": 3
28725
+ // Within the tier, the bigger place wins the space: without this the
28726
+ // tie broke on data order and "West Gloucester" beat Gloucester.
28727
+ "symbol-sort-key": ["*", -1, ["coalesce", ["get", "pop"], 0]]
28277
28728
  },
28278
- paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28729
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.4 }
28279
28730
  }));
28280
28731
  layers.push(L({
28281
- id: "chrtland-poi",
28732
+ id: "chrtland-town",
28282
28733
  type: "symbol",
28283
28734
  source: src,
28284
- "source-layer": "land_poi",
28285
- minzoom: 13,
28286
- filter: FITS,
28735
+ "source-layer": "land_town",
28736
+ minzoom: 9,
28737
+ filter: [
28738
+ "all",
28739
+ FITS,
28740
+ ["<", ["coalesce", ["get", "prank"], 0], 10],
28741
+ ["!=", ["get", "enc_dup"], true]
28742
+ ],
28287
28743
  layout: {
28288
- "text-field": ["get", "name"],
28289
- "text-font": FONT_ITALIC,
28290
- "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
28291
- "text-offset": anchorOffset(0.7),
28744
+ "text-field": [
28745
+ "case",
28746
+ ["==", ["get", "upcase"], true],
28747
+ ["upcase", ["get", "name"]],
28748
+ ["get", "name"]
28749
+ ],
28750
+ "text-font": FONT,
28751
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 10, 14, 13.5],
28752
+ "text-letter-spacing": ["case", ["==", ["get", "upcase"], true], 0.14, 0],
28753
+ "text-offset": anchorOffset(0.75),
28292
28754
  "text-anchor": ANCHOR,
28293
- "text-padding": 5,
28755
+ "text-padding": 6,
28294
28756
  "text-allow-overlap": false,
28295
- "symbol-sort-key": 5
28757
+ "symbol-sort-key": 2
28296
28758
  },
28297
28759
  paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28298
28760
  }));
28299
- return layers;
28300
- }
28301
-
28302
- // src/atlas/band-priority.ts
28303
- var BAND_PRIORITY_RANGES = {
28304
- "1": { minzoom: 0 },
28305
- "2": { minzoom: 5 },
28306
- "3": { minzoom: 7 },
28307
- "4": { minzoom: 9 },
28308
- "5": { minzoom: 11 },
28309
- "6": { minzoom: 11 }
28310
- };
28311
- var BAND_DATA_MINZOOM = {
28312
- "1": 0,
28313
- "2": 5,
28314
- "3": 7,
28315
- "4": 9,
28316
- "5": 11,
28317
- "6": 13
28318
- };
28319
- function dominatedVariantMaxzoom(D) {
28320
- return Math.max(BAND_PRIORITY_RANGES[D].minzoom, BAND_DATA_MINZOOM[D]);
28321
- }
28322
- var DOMINANCE_EXEMPT_FILL_CLASSES = /* @__PURE__ */ new Set([
28323
- "LNDARE",
28324
- "DEPARE",
28325
- "UNSARE",
28326
- "LAKARE",
28327
- "DRGARE",
28328
- "SEAARE"
28329
- ]);
28330
- function isExemptOpaqueFill(layer) {
28331
- if (layer.type !== "fill") return false;
28332
- for (const cls of DOMINANCE_EXEMPT_FILL_CLASSES) {
28333
- if (layer.id.startsWith(`s52-${cls}-`)) return true;
28334
- }
28335
- return false;
28336
- }
28337
- var FILL_FLOOR_FREE_BANDS = /* @__PURE__ */ new Set([
28338
- "1",
28339
- "2",
28340
- "3"
28341
- ]);
28342
- function bandVariantMinzoom(layer, B) {
28343
- const classMinzoom = layer.minzoom ?? 0;
28344
- if (isExemptOpaqueFill(layer) && FILL_FLOOR_FREE_BANDS.has(B)) {
28345
- return classMinzoom;
28346
- }
28347
- return Math.max(classMinzoom, BAND_PRIORITY_RANGES[B].minzoom);
28348
- }
28349
- var BAND_DRAW_ORDER = ["1", "2", "3", "4", "5", "6"];
28350
- var CHART_BAND_CODES = {
28351
- Overview: ["1"],
28352
- General: ["2"],
28353
- Coastal: ["3"],
28354
- Approach: ["4"],
28355
- Harbor: ["5", "6"]
28356
- };
28357
- var CHART_CASCADE_CODES = {
28358
- Overview: ["1"],
28359
- General: ["1", "2"],
28360
- Coastal: ["1", "2", "3"],
28361
- Approach: ["1", "2", "3", "4"],
28362
- Harbor: ["1", "2", "3", "4", "5", "6"]
28363
- };
28364
- var CHRT_STYLE_MODES = [
28365
- "continuous",
28366
- ...Object.keys(CHART_BAND_CODES).map(
28367
- (band) => `chart:${band}`
28368
- )
28369
- ];
28370
- function normalizeStyleMode(raw) {
28371
- if (raw === null || raw === void 0 || raw === "") return "continuous";
28372
- const lower = raw.toLowerCase();
28373
- return CHRT_STYLE_MODES.find((mode) => mode.toLowerCase() === lower) ?? null;
28374
- }
28375
- function styleModeBand(mode) {
28376
- return mode === "continuous" ? null : mode.slice("chart:".length);
28377
- }
28378
- function bandFilterExpression(codes) {
28379
- const list = Array.isArray(codes) ? codes : [codes];
28380
- if (list.length === 1) {
28381
- return ["==", ["slice", ["get", "_cell_id"], 2, 3], list[0]];
28382
- }
28383
- return ["in", ["slice", ["get", "_cell_id"], 2, 3], ["literal", [...list]]];
28384
- }
28385
- function combineWithBandFilter(baseFilter, bandFilter) {
28386
- if (!baseFilter) return bandFilter;
28387
- if (baseFilter[0] === "all") {
28388
- return ["all", ...baseFilter.slice(1), bandFilter];
28761
+ layers.push(L({
28762
+ id: "chrtland-summit",
28763
+ type: "symbol",
28764
+ source: src,
28765
+ "source-layer": "land_summit",
28766
+ minzoom: 11.5,
28767
+ filter: ["all", FITS, [">=", ["get", "peak_m"], 40]],
28768
+ layout: {
28769
+ "text-field": ["concat", ["get", "name"], "\n", ["to-string", ["get", "peak_m"]], " m"],
28770
+ "text-font": FONT,
28771
+ "text-size": ["interpolate", ["linear"], ["zoom"], 11.5, 8.5, 14, 11],
28772
+ "text-offset": anchorOffset(0.8),
28773
+ "text-anchor": ANCHOR,
28774
+ "text-padding": 5,
28775
+ "text-allow-overlap": false,
28776
+ "symbol-sort-key": 3
28777
+ },
28778
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28779
+ }));
28780
+ layers.push(L({
28781
+ id: "chrtland-poi",
28782
+ type: "symbol",
28783
+ source: src,
28784
+ "source-layer": "land_poi",
28785
+ minzoom: 13,
28786
+ filter: FITS,
28787
+ layout: {
28788
+ "text-field": ["get", "name"],
28789
+ "text-font": FONT_ITALIC,
28790
+ "text-size": ["interpolate", ["linear"], ["zoom"], 13, 8, 14, 9.5],
28791
+ "text-offset": anchorOffset(0.7),
28792
+ "text-anchor": ANCHOR,
28793
+ "text-padding": 5,
28794
+ "text-allow-overlap": false,
28795
+ "symbol-sort-key": 5
28796
+ },
28797
+ paint: { "text-color": T.label, "text-halo-color": T.halo, "text-halo-width": 1.2 }
28798
+ }));
28799
+ for (const layer of layers) {
28800
+ const l = layer;
28801
+ if (!LAND_SURFACE_LAYERS(l.id)) continue;
28802
+ l.minzoom = Math.max(l.minzoom ?? 0, LAND_SURFACE_MINZOOM);
28389
28803
  }
28390
- return ["all", baseFilter, bandFilter];
28804
+ return layers;
28391
28805
  }
28392
- var NOT_DOMINATED_FILTER = [
28393
- "all",
28394
- ["!", ["has", "_dominated_by_min"]],
28395
- ["!=", ["get", "_dominated"], true]
28396
- ];
28397
- function dominatedByMinFilter(D) {
28398
- const dInt = parseInt(D, 10);
28399
- if (D === "6") {
28400
- return [
28401
- "any",
28402
- ["==", ["get", "_dominated_by_min"], dInt],
28403
- ["==", ["get", "_dominated"], true]
28404
- ];
28806
+
28807
+ // src/atlas/base-layers.ts
28808
+ var NOCOVERAGE_PATTERN = "chrt:hatch-nocoverage";
28809
+ var UNCHARTED_PATTERN = "chrt:hatch-uncharted";
28810
+ var BASE_THEMES = {
28811
+ DAY: {
28812
+ earth: "#2A3439",
28813
+ water: "#1B2328",
28814
+ river: "#24313A",
28815
+ boundary: "#4A575E",
28816
+ jurisdiction: "#E8D9B0",
28817
+ // CHRTland's own tan — one land colour
28818
+ inlandWater: "#B9CFDD",
28819
+ // muted, unmistakably not a charted blue
28820
+ // State furniture. These three were referenced by the layers but missing
28821
+ // from this table (and its type — the long-standing TS2339 trio), so the
28822
+ // lines shipped on MapLibre's DEFAULT black. Now deliberate: a hairline
28823
+ // a shade deeper than the tan, quiet caps, tan halo.
28824
+ stateLine: "#C4B389",
28825
+ stateLabel: "#8E8468",
28826
+ stateHalo: "#E8D9B0",
28827
+ // Rough-lake furniture (owner ask 2026-08-28: "the lakes need labels,
28828
+ // and shorelines drawn"): a shoreline a shade deeper than the rough
28829
+ // water, and a name in the water's own voice, haloed by the water.
28830
+ inlandShore: "#8FA9BA",
28831
+ inlandLabel: "#54707F",
28832
+ inlandHalo: "#B9CFDD"
28833
+ },
28834
+ DUSK: {
28835
+ earth: "#232B30",
28836
+ water: "#161D21",
28837
+ river: "#1E2930",
28838
+ boundary: "#3E4A50",
28839
+ jurisdiction: "#C9BC96",
28840
+ inlandWater: "#93A9B7",
28841
+ stateLine: "#A6976F",
28842
+ stateLabel: "#776E55",
28843
+ stateHalo: "#C9BC96",
28844
+ inlandShore: "#74899A",
28845
+ inlandLabel: "#41586A",
28846
+ inlandHalo: "#93A9B7"
28847
+ },
28848
+ NIGHT: {
28849
+ earth: "#1A2125",
28850
+ water: "#10161A",
28851
+ river: "#182228",
28852
+ boundary: "#333E44",
28853
+ jurisdiction: "#8F866C",
28854
+ inlandWater: "#5F7280",
28855
+ stateLine: "#6B6350",
28856
+ stateLabel: "#565040",
28857
+ stateHalo: "#8F866C",
28858
+ inlandShore: "#4A5C68",
28859
+ inlandLabel: "#8CA2B0",
28860
+ inlandHalo: "#5F7280"
28405
28861
  }
28406
- return ["==", ["get", "_dominated_by_min"], dInt];
28407
- }
28408
- function withinBboxFilter(bbox) {
28409
- const [w, s] = bbox.sw;
28410
- const [e, n] = bbox.ne;
28862
+ };
28863
+ function baseLayers(opts = {}) {
28864
+ const palette = opts.palette ?? "DAY";
28865
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
28866
+ const src = opts.sourceId ?? BASE_REGION;
28867
+ const L = (o) => o;
28411
28868
  return [
28412
- "within",
28413
- {
28414
- type: "Polygon",
28415
- coordinates: [
28416
- [
28417
- [w, s],
28418
- [e, s],
28419
- [e, n],
28420
- [w, n],
28421
- [w, s]
28422
- ]
28423
- ]
28424
- }
28869
+ // 1. Geography — the world as it is, in the quiet dark voice. The OCEAN
28870
+ // is a background, not a polygon: the archive ships land and lakes,
28871
+ // and "everything else is sea" is exactly what a background expresses
28872
+ // — geometry-free, gap-free at every antimeridian and pole. Without
28873
+ // it the sea outside coverage was bare S-52 NODTA, a light grey that
28874
+ // read as "charted ground" with a hatch on it.
28875
+ L({
28876
+ id: "chrtbase-ocean",
28877
+ type: "background",
28878
+ paint: { "background-color": T.water }
28879
+ }),
28880
+ L({
28881
+ id: "chrtbase-earth",
28882
+ type: "fill",
28883
+ source: src,
28884
+ "source-layer": "base_earth",
28885
+ paint: { "fill-color": T.earth }
28886
+ }),
28887
+ L({
28888
+ id: "chrtbase-water",
28889
+ type: "fill",
28890
+ source: src,
28891
+ "source-layer": "base_water",
28892
+ paint: { "fill-color": T.water }
28893
+ }),
28894
+ L({
28895
+ id: "chrtbase-river",
28896
+ type: "line",
28897
+ source: src,
28898
+ "source-layer": "base_river",
28899
+ minzoom: 9,
28900
+ paint: {
28901
+ "line-color": T.river,
28902
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.6, 14, 1.6]
28903
+ }
28904
+ }),
28905
+ L({
28906
+ id: "chrtbase-boundary",
28907
+ type: "line",
28908
+ source: src,
28909
+ "source-layer": "base_boundary",
28910
+ paint: { "line-color": T.boundary, "line-width": 0.6, "line-opacity": 0.8 }
28911
+ }),
28912
+ // 2. THE DEFAULT: no coverage, everywhere, as a geometry-free background.
28913
+ // Constant opacity by design — see the header. What kills it is being
28914
+ // painted OVER, never fading.
28915
+ L({
28916
+ id: "chrtbase-nocoverage",
28917
+ type: "background",
28918
+ paint: { "background-pattern": NOCOVERAGE_PATTERN, "background-opacity": 0.55 }
28919
+ }),
28920
+ // 3. The jurisdiction: countries whose charts CHRT carries go tan — the
28921
+ // same tan as CHRTland, so "land in our world" is ONE colour. Opaque,
28922
+ // so it takes the crosshatch off its ground entirely.
28923
+ L({
28924
+ id: "chrtbase-jurisdiction",
28925
+ type: "fill",
28926
+ source: src,
28927
+ "source-layer": "jurisdiction",
28928
+ paint: { "fill-color": T.jurisdiction }
28929
+ }),
28930
+ // 4. States — internal boundaries and names, inside the jurisdiction
28931
+ // only (owner decision 2026-08-28). A hairline a shade deeper than the
28932
+ // tan, and quiet letter-spaced caps that fade as harbour detail takes
28933
+ // over. The one deliberate exception to no-names-on-the-basemap:
28934
+ // ADMIN furniture, nothing else — see the fence test.
28935
+ L({
28936
+ id: "chrtbase-state-boundary",
28937
+ type: "line",
28938
+ source: src,
28939
+ "source-layer": "state_boundary",
28940
+ minzoom: 4,
28941
+ paint: {
28942
+ "line-color": T.stateLine,
28943
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1],
28944
+ "line-dasharray": [3, 2],
28945
+ "line-opacity": 0.8
28946
+ }
28947
+ }),
28948
+ // 5. Country borders, drawn to be HOISTED above the whole chart by the
28949
+ // generator (owner, 2026-08-31: "where there is charted land, can we
28950
+ // add country borders on top of everything? I want it clear where
28951
+ // there is a boarder."). Sourced from NE boundary_lines_land — land
28952
+ // borders only, never coastline — so riding on top cannot stroke a
28953
+ // coast. Dashed so it reads as an administrative line, not chart
28954
+ // linework.
28955
+ L({
28956
+ id: "chrtbase-country-boundary",
28957
+ type: "line",
28958
+ source: src,
28959
+ "source-layer": "country_boundary",
28960
+ paint: {
28961
+ "line-color": T.boundary,
28962
+ "line-width": ["interpolate", ["linear"], ["zoom"], 3, 0.8, 8, 1.4, 12, 2.2],
28963
+ "line-dasharray": [4, 2],
28964
+ "line-opacity": 0.9
28965
+ }
28966
+ }),
28967
+ L({
28968
+ id: "chrtbase-state-label",
28969
+ type: "symbol",
28970
+ source: src,
28971
+ "source-layer": "state_label",
28972
+ minzoom: 4,
28973
+ maxzoom: 10,
28974
+ layout: {
28975
+ "text-field": ["upcase", ["get", "name"]],
28976
+ "text-font": ["Open Sans Regular"],
28977
+ "text-size": ["interpolate", ["linear"], ["zoom"], 4, 9, 8, 12],
28978
+ "text-letter-spacing": 0.25,
28979
+ "text-allow-overlap": false,
28980
+ "text-padding": 8
28981
+ },
28982
+ paint: {
28983
+ "text-color": T.stateLabel,
28984
+ "text-halo-color": T.stateHalo,
28985
+ "text-halo-width": 1.2,
28986
+ "text-opacity": ["interpolate", ["linear"], ["zoom"], 4, 0.7, 8, 0.85, 10, 0.4]
28987
+ }
28988
+ })
28425
28989
  ];
28426
28990
  }
28427
- function worldMinusBboxPolygon(bbox) {
28428
- const [w, s] = bbox.sw;
28429
- const [e, n] = bbox.ne;
28430
- return {
28431
- type: "Polygon",
28432
- coordinates: [
28433
- [
28434
- [-180, -85],
28435
- [180, -85],
28436
- [180, 85],
28437
- [-180, 85],
28438
- [-180, -85]
28439
- ],
28440
- // Inner ring (hole) — winding opposite to outer ring per GeoJSON
28441
- // spec. MapLibre is permissive but we follow strict winding.
28442
- [
28443
- [w, s],
28444
- [w, n],
28445
- [e, n],
28446
- [e, s],
28447
- [w, s]
28448
- ]
28449
- ]
28450
- };
28991
+ function baseInlandWaterLayers(opts = {}) {
28992
+ const palette = opts.palette ?? "DAY";
28993
+ const T = BASE_THEMES[palette] ?? BASE_THEMES.DAY;
28994
+ const src = opts.sourceId ?? BASE_REGION;
28995
+ const L = (o) => o;
28996
+ const NE_TIER = ["!", ["has", "kind"]];
28997
+ const PLANET_TIER = ["has", "kind"];
28998
+ const HANDOVER = 9;
28999
+ return [
29000
+ // ── The fill pair, SPLIT BY TIER at the handover ────────────────────────
29001
+ //
29002
+ // Both tiers share `inland_water` (tile-join concatenates the same layer
29003
+ // name), and an unsplit fill draws BOTH — which means the NE polygon is
29004
+ // painted over the planet one at every zoom. That is not a cosmetic
29005
+ // double-draw: NE 10m carries no interior rings, so its blob has no
29006
+ // ISLANDS in it, and painting it over the planet's holed polygons buries
29007
+ // every island in the lake under "uncharted water". Owner-reported at
29008
+ // Winnipesaukee z11.7 ("there is land (and islands) being covered by the
29009
+ // 'no chart here' boundary"), and measured: rendering the planet tier
29010
+ // alone brings back the whole archipelago — Rattlesnake, Bear, Long
29011
+ // Island, Diamond, the Barndoors — while the NE tier alone is a faceted
29012
+ // blob whose edges bear no relation to the shoreline drawn beneath it.
29013
+ //
29014
+ // So each tier owns its own zoom range, the same handover the shoreline
29015
+ // below uses. Known residue, measured over the whole archive: of the 141
29016
+ // NE lakes we still carry, 138 have planet water beneath them; the
29017
+ // exceptions are two unnamed western-Alaska tundra lakes and a 2e-5 deg²
29018
+ // Lake Michigan sliver, which lose their rough fill above z9 because the
29019
+ // planet has no `water` polygon there at all (checked against the raw
29020
+ // pre-clip NDJSON — not a clip artefact).
29021
+ L({
29022
+ id: "chrtbase-inland-water",
29023
+ type: "fill",
29024
+ source: src,
29025
+ "source-layer": "inland_water",
29026
+ filter: NE_TIER,
29027
+ maxzoom: HANDOVER,
29028
+ paint: { "fill-color": T.inlandWater }
29029
+ }),
29030
+ L({
29031
+ id: "chrtbase-inland-hatch",
29032
+ type: "fill",
29033
+ source: src,
29034
+ "source-layer": "inland_water",
29035
+ filter: NE_TIER,
29036
+ maxzoom: HANDOVER,
29037
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29038
+ }),
29039
+ L({
29040
+ id: "chrtbase-inland-water-detail",
29041
+ type: "fill",
29042
+ source: src,
29043
+ "source-layer": "inland_water",
29044
+ filter: PLANET_TIER,
29045
+ minzoom: HANDOVER,
29046
+ paint: { "fill-color": T.inlandWater }
29047
+ }),
29048
+ L({
29049
+ id: "chrtbase-inland-hatch-detail",
29050
+ type: "fill",
29051
+ source: src,
29052
+ "source-layer": "inland_water",
29053
+ filter: PLANET_TIER,
29054
+ minzoom: HANDOVER,
29055
+ paint: { "fill-pattern": UNCHARTED_PATTERN, "fill-opacity": 0.7 }
29056
+ }),
29057
+ // The rough shoreline (owner ask 2026-08-28). Drawn ABOVE the hatch so
29058
+ // the edge stays crisp through the texture, and BELOW the charted-water
29059
+ // repaint clones the generator splices in after it — so wherever the
29060
+ // chart repaints its own water, the rough NE10m line yields to the
29061
+ // chart's COALNE instead of double-striking it.
29062
+ //
29063
+ // NE-tier geometry ONLY (`kind` absent): the planet detail tier joins
29064
+ // the same source-layer from z9 as MANY polygons whose chunk-clip seams
29065
+ // rejoin invisibly as FILLS — but a line layer strokes every seam, and
29066
+ // Winnipesaukee rendered as a mesh of internal edges. Planet features
29067
+ // carry `kind` (the INLAND_KINDS filter's own field); NE's carry only
29068
+ // `name`, so the absence of `kind` IS the tier discriminator.
29069
+ L({
29070
+ id: "chrtbase-inland-shore",
29071
+ type: "line",
29072
+ source: src,
29073
+ "source-layer": "inland_water",
29074
+ filter: NE_TIER,
29075
+ // Hands the shoreline over to the planet tier at ITS floor. Both
29076
+ // tiers live in `inland_water` from z9 up (tile-join concatenates the
29077
+ // same layer name), and their FILLS merge invisibly — but two
29078
+ // outlines do not: NE 10m generalises Winnipesaukee's shape, so its
29079
+ // outline ran straight across the middle of the lake the planet had
29080
+ // drawn correctly around. The coarse line is right only while it is
29081
+ // the only shape we have.
29082
+ maxzoom: HANDOVER,
29083
+ paint: {
29084
+ "line-color": T.inlandShore,
29085
+ "line-width": ["interpolate", ["linear"], ["zoom"], 4, 0.5, 10, 1.1, 14, 1.6],
29086
+ "line-opacity": 0.9
29087
+ }
29088
+ }),
29089
+ // The PLANET tier's shoreline (owner ask 2026-08-31: "uncharted small
29090
+ // lakes render with no outline"). It reads its own `inland_shore` layer,
29091
+ // NOT `inland_water`, and that indirection is the whole fix: the mesh
29092
+ // that forced the `!has kind` fence above is a CLIP artefact — an MVT
29093
+ // feature arrives already cut to its own z10 tile, so stroking the
29094
+ // polygon strokes every tile boundary it crosses. The build now walks
29095
+ // those rings, drops the axis-aligned runs that sit on the read zoom's
29096
+ // tile grid, and emits what is left as lines (measured on one New
29097
+ // England chunk: 61 long axis-aligned mesh edges → 5, and the 5 survivors
29098
+ // are 0.02–0.5 tiles off the grid, i.e. real straight water edges).
29099
+ //
29100
+ // Thinner and quieter than the NE line: this is finer geometry describing
29101
+ // water nobody has surveyed, and approximate data should look
29102
+ // approximate.
29103
+ L({
29104
+ id: "chrtbase-inland-shore-detail",
29105
+ type: "line",
29106
+ source: src,
29107
+ "source-layer": "inland_shore",
29108
+ minzoom: HANDOVER,
29109
+ paint: {
29110
+ "line-color": T.inlandShore,
29111
+ "line-width": ["interpolate", ["linear"], ["zoom"], 9, 0.5, 12, 0.9, 15, 1.2],
29112
+ "line-opacity": 0.85
29113
+ }
29114
+ }),
29115
+ // The lake's name (owner ask 2026-08-28) — the second scoped exception
29116
+ // to no-names-on-the-basemap, after the states: WATER-BODY names only.
29117
+ // No maxzoom: an uncharted lake keeps its name at every zoom; over
29118
+ // charted lakes the chart's own SEAARE label takes over at detail zooms
29119
+ // and collision culls the echo.
29120
+ //
29121
+ // The archive's `inland_water_label` anchors (v3+). v3 shipped ONE
29122
+ // PointOnSurface anchor per lake, and a big lake's single anchor pans
29123
+ // off-viewport — Winnipesaukee at z11.7 read nameless with its name two
29124
+ // tiles west of the screen (owner-reported). Labelling the polygons
29125
+ // instead is NOT the fix: tippecanoe's coalescing drops the NE polygon
29126
+ // from some z11 tiles (measured: 11/619/747 carries only planet pieces),
29127
+ // so a polygon-sourced label vanishes tile by tile. v4 therefore bakes a
29128
+ // deterministic anchor GRID into big lakes — the paper-chart convention
29129
+ // of repeating a name along a large water body — and this one layer
29130
+ // renders however many anchors are in view; collision culls the rest.
29131
+ // Planet-tier water-body NAMES (owner ask 2026-08-31: NE 10m names only
29132
+ // 113 US lakes). Emitted BELOW the NE layer on purpose — MapLibre places
29133
+ // symbols topmost-layer-first, so the big NE lake wins collision against
29134
+ // the pond beside it, and both lose to the chart's own labels (every ENC
29135
+ // class is emitted after this pair).
29136
+ //
29137
+ // The size threshold is OSM's own `min_zoom`, carried through the build:
29138
+ // Lake Winnipesaukee reads 10, Squam Lake 12, so a pond simply is not in
29139
+ // the filter until the user is close enough for it to be the answer to
29140
+ // "what is that water". `["zoom"]` in a filter evaluates at integer zoom
29141
+ // levels and removes the feature from placement entirely — verified
29142
+ // against maplibre-gl-native rather than assumed, because a text-size-0
29143
+ // gate would still claim a collision slot.
29144
+ //
29145
+ // The tiers are separate LAYERS, not a filtered split of one — see the
29146
+ // builder for why: an archive that a not-yet-deployed style would
29147
+ // mis-render cannot be promoted first, and this one has to be.
29148
+ L({
29149
+ id: "chrtbase-inland-label-detail",
29150
+ type: "symbol",
29151
+ source: src,
29152
+ "source-layer": "inland_water_label_detail",
29153
+ minzoom: HANDOVER,
29154
+ filter: [">=", ["zoom"], ["get", "min_zoom"]],
29155
+ layout: {
29156
+ "text-field": ["get", "name"],
29157
+ "text-font": ["Open Sans Regular"],
29158
+ "text-size": ["interpolate", ["linear"], ["zoom"], 9, 9, 12, 10.5, 15, 11],
29159
+ "text-max-width": 8,
29160
+ "text-allow-overlap": false,
29161
+ "text-padding": 6,
29162
+ // Bigger water first within the tier — min_zoom IS the size rank.
29163
+ "symbol-sort-key": ["get", "min_zoom"]
29164
+ },
29165
+ paint: {
29166
+ "text-color": T.inlandLabel,
29167
+ "text-halo-color": T.inlandHalo,
29168
+ "text-halo-width": 1,
29169
+ "text-opacity": 0.9
29170
+ }
29171
+ }),
29172
+ L({
29173
+ id: "chrtbase-inland-label",
29174
+ type: "symbol",
29175
+ source: src,
29176
+ "source-layer": "inland_water_label",
29177
+ minzoom: 5,
29178
+ layout: {
29179
+ "text-field": ["get", "name"],
29180
+ "text-font": ["Open Sans Regular"],
29181
+ "text-size": ["interpolate", ["linear"], ["zoom"], 5, 9.5, 10, 12],
29182
+ "text-max-width": 7,
29183
+ "text-allow-overlap": false,
29184
+ // Zoom-scaled spacing for the anchor GRID: at z8.9 the 0.08-deg
29185
+ // anchors sit ~54 px apart and a flat padding let six copies of
29186
+ // "Lake Ontario" crowd one screen (owner-reported 2026-08-30).
29187
+ // Wide collision boxes at low zoom thin the grid to the repeat-
29188
+ // by-convention cadence; by z12 the anchors are ~850 px apart and
29189
+ // the padding relaxes to normal.
29190
+ "text-padding": ["interpolate", ["linear"], ["zoom"], 5, 80, 9, 64, 12, 16]
29191
+ },
29192
+ paint: {
29193
+ "text-color": T.inlandLabel,
29194
+ "text-halo-color": T.inlandHalo,
29195
+ "text-halo-width": 1.1
29196
+ }
29197
+ })
29198
+ ];
28451
29199
  }
28452
29200
 
28453
29201
  // src/atlas/band-collapse.ts
28454
- function collapsedDominanceGate(B, minzoom) {
28455
- const clauses = [NOT_DOMINATED_FILTER];
29202
+ function collapsedDominanceGate(B, minzoom, notDominatedCeiling = null) {
29203
+ const clauses = [
29204
+ notDominatedCeiling !== null && notDominatedCeiling > minzoom ? ["all", NOT_DOMINATED_FILTER, ["<", ["zoom"], notDominatedCeiling]] : NOT_DOMINATED_FILTER
29205
+ ];
28456
29206
  const bInt = parseInt(B, 10);
28457
29207
  for (const D of BAND_DRAW_ORDER) {
28458
29208
  const dInt = parseInt(D, 10);
@@ -28481,7 +29231,7 @@ function makeCollapsedBandVariant(layer, B, isExempt) {
28481
29231
  filter: combineWithBandFilter(layer.filter, [
28482
29232
  "all",
28483
29233
  bandFilterExpression(B),
28484
- collapsedDominanceGate(B, minzoom)
29234
+ collapsedDominanceGate(B, minzoom, generalisedAreaCeiling(layer, B))
28485
29235
  ]),
28486
29236
  minzoom
28487
29237
  };
@@ -28616,6 +29366,7 @@ function makeBandVariants(layer, B) {
28616
29366
  delete exempt.maxzoom;
28617
29367
  return [exempt];
28618
29368
  }
29369
+ const ceiling = generalisedAreaCeiling(layer, B);
28619
29370
  const notDom = {
28620
29371
  ...layer,
28621
29372
  id: `${layer.id}-band${B}`,
@@ -28626,6 +29377,7 @@ function makeBandVariants(layer, B) {
28626
29377
  minzoom
28627
29378
  };
28628
29379
  delete notDom.maxzoom;
29380
+ if (ceiling !== null && ceiling > minzoom) notDom.maxzoom = ceiling;
28629
29381
  variants.push(notDom);
28630
29382
  const bInt = parseInt(B, 10);
28631
29383
  for (const D of BAND_DRAW_ORDER) {
@@ -28648,14 +29400,21 @@ function makeBandVariants(layer, B) {
28648
29400
  return variants;
28649
29401
  }
28650
29402
  var CELL_BAND_OPACITY_CURVE = {
28651
- "1": [[3, 0.5], [5, 0.3], [16, 0.15]],
28652
- "2": [[5, 0.5], [7, 0.3], [16, 0.15]],
28653
- "3": [[7, 0.55], [9, 0.35], [16, 0.18]],
28654
- "4": [[9, 0.55], [11, 0.35], [16, 0.2]],
28655
- "5": [[11, 0.6], [13, 0.35], [18, 0.25]],
28656
- "6": [[13, 0.75], [16, 0.55], [22, 0.45]]
29403
+ "1": [[3, 0.5], [5, 0.42], [16, 0.34]],
29404
+ "2": [[5, 0.52], [7, 0.44], [16, 0.34]],
29405
+ "3": [[7, 0.55], [9, 0.46], [16, 0.36]],
29406
+ "4": [[9, 0.58], [11, 0.5], [16, 0.42]],
29407
+ "5": [[11, 0.62], [13, 0.52], [18, 0.44]],
29408
+ "6": [[13, 0.75], [16, 0.6], [22, 0.5]]
28657
29409
  };
28658
- function fanOrientationLabelByCellBand(label, bandFan) {
29410
+ var ORIENTATION_OPACITY_SCALE = {
29411
+ LNDARE: 1.45,
29412
+ LNDRGN: 1.4,
29413
+ BUAARE: 1.25
29414
+ };
29415
+ var ORIENTATION_OPACITY_MAX = 0.92;
29416
+ var ORIENTATION_BOOST_MINZOOM = 11;
29417
+ function fanOrientationLabelByCellBand(label, bandFan, objectClass) {
28659
29418
  const cellBandExpr = [
28660
29419
  "slice",
28661
29420
  ["coalesce", ["get", "_cell_id"], "??9???"],
@@ -28663,11 +29422,15 @@ function fanOrientationLabelByCellBand(label, bandFan) {
28663
29422
  3
28664
29423
  ];
28665
29424
  const variants = [];
29425
+ const scale = objectClass ? ORIENTATION_OPACITY_SCALE[objectClass] ?? 1 : 1;
28666
29426
  for (const code of BAND_DRAW_ORDER) {
28667
29427
  const range = BAND_PRIORITY_RANGES[code];
28668
29428
  const curve = CELL_BAND_OPACITY_CURVE[code];
28669
29429
  const opacityExpr = ["interpolate", ["linear"], ["zoom"]];
28670
- for (const [z, o] of curve) opacityExpr.push(z, o);
29430
+ for (const [z, o] of curve) {
29431
+ const s = z >= ORIENTATION_BOOST_MINZOOM ? scale : 1;
29432
+ opacityExpr.push(z, Math.round(Math.min(o * s, ORIENTATION_OPACITY_MAX) * 100) / 100);
29433
+ }
28671
29434
  const cellFiltered = combineWithBandFilter(
28672
29435
  label.filter,
28673
29436
  ["==", cellBandExpr, code]
@@ -28714,6 +29477,33 @@ function fanOrientationLabelByCellBand(label, bandFan) {
28714
29477
  }
28715
29478
  return variants;
28716
29479
  }
29480
+ var MAJOR_WATER_TEXT_PADDING = [
29481
+ "interpolate",
29482
+ ["linear"],
29483
+ ["zoom"],
29484
+ 5,
29485
+ 24,
29486
+ 9,
29487
+ 20,
29488
+ 13,
29489
+ 16,
29490
+ 16,
29491
+ 14
29492
+ ];
29493
+ function splitOrientationFan(fan, hoistedFilter, watermarkFilter, hoistedLayout) {
29494
+ return {
29495
+ hoist: fan.map((l) => ({
29496
+ ...l,
29497
+ id: `${l.id}-major`,
29498
+ filter: combineWithBandFilter(l.filter, hoistedFilter),
29499
+ ...hoistedLayout ? { layout: { ...l.layout ?? {}, ...hoistedLayout } } : {}
29500
+ })),
29501
+ watermark: fan.map((l) => ({
29502
+ ...l,
29503
+ filter: combineWithBandFilter(l.filter, watermarkFilter)
29504
+ }))
29505
+ };
29506
+ }
28717
29507
  function byLabelPriorityBottomUp(bundles) {
28718
29508
  return [...bundles].sort(
28719
29509
  (a, b) => getLabelPriority(b.objectClass) - getLabelPriority(a.objectClass)
@@ -28775,13 +29565,45 @@ function generateStyle(palette = "DAY", opts = {}) {
28775
29565
  paint: { "background-color": resolveColor("NODTA", palette) }
28776
29566
  };
28777
29567
  const layers = [backgroundLayer];
29568
+ if (opts.base) {
29569
+ const base = baseLayers({ palette });
29570
+ const coverageGround = {
29571
+ id: "s52-coverage-ground",
29572
+ type: "fill",
29573
+ source: sourceId,
29574
+ "source-layer": "M_COVR",
29575
+ filter: [
29576
+ "all",
29577
+ ["any", ["==", ["geometry-type"], "Polygon"], ["==", ["geometry-type"], "MultiPolygon"]],
29578
+ ["==", ["to-number", ["coalesce", ["get", "CATCOV"], 0]], 1]
29579
+ ],
29580
+ paint: { "fill-color": resolveColor("DEPDW", palette) },
29581
+ metadata: { "s52:objectClass": "M_COVR", "s52:role": "coverage-ground" }
29582
+ };
29583
+ const jurisdictionAt = base.findIndex(
29584
+ (l) => l.id === "chrtbase-jurisdiction"
29585
+ );
29586
+ if (jurisdictionAt === -1) {
29587
+ layers.push(...base, coverageGround);
29588
+ } else {
29589
+ layers.push(
29590
+ ...base.slice(0, jurisdictionAt),
29591
+ coverageGround,
29592
+ ...base.slice(jurisdictionAt)
29593
+ );
29594
+ }
29595
+ }
28778
29596
  if (opts.land) {
28779
29597
  layers.push(...landLayers({
28780
29598
  palette,
28781
29599
  ...opts.relief ? { relief: true } : {}
28782
29600
  }));
28783
29601
  }
29602
+ if (opts.base) {
29603
+ layers.push(...baseInlandWaterLayers({ palette }));
29604
+ }
28784
29605
  const labelLayers = [];
29606
+ const majorWaterLayers = [];
28785
29607
  const chartCascade = opts.chartMode ? CHART_CASCADE_CODES[opts.chartMode.band] : null;
28786
29608
  const chartRequestedCodes = opts.chartMode ? CHART_BAND_CODES[opts.chartMode.band] : null;
28787
29609
  const bandFan = opts.bandFan ?? "collapsed";
@@ -28897,13 +29719,44 @@ function generateStyle(palette = "DAY", opts = {}) {
28897
29719
  }
28898
29720
  }
28899
29721
  }
29722
+ {
29723
+ const TERMINAL_FILL_RE = /^s52-(?:LNDARE|DEPARE)-fill-band[456]$/;
29724
+ for (const l of layers) {
29725
+ const lay = l;
29726
+ if (!TERMINAL_FILL_RE.test(lay.id)) continue;
29727
+ const floor = lay.minzoom ?? 0;
29728
+ if (floor <= 0) continue;
29729
+ lay.filter = [
29730
+ "all",
29731
+ lay.filter,
29732
+ ["any", [">=", ["zoom"], floor], ["==", ["get", "_terminal_fill"], true]]
29733
+ ];
29734
+ delete lay.minzoom;
29735
+ }
29736
+ }
28900
29737
  if (opts.labels === "placename") {
28901
29738
  labelLayers.push(...buildPlaceNameLayers(sourceId));
28902
29739
  } else {
28903
29740
  for (const { objectClass, label } of bundles) {
28904
29741
  if (!label) continue;
28905
29742
  if (getLabelBehavior(objectClass) === "orientation-cross-band") {
28906
- labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan));
29743
+ if (objectClass === "LNDARE" || objectClass === "SEAARE") {
29744
+ const Z = ISLAND_LABEL_HOIST_MINZOOM;
29745
+ const { hoist, watermark } = objectClass === "LNDARE" ? splitOrientationFan(
29746
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29747
+ [">=", ["zoom"], Z],
29748
+ ["<", ["zoom"], Z]
29749
+ ) : splitOrientationFan(
29750
+ fanOrientationLabelByCellBand(label, bandFan, objectClass),
29751
+ ["<=", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29752
+ [">", buildSeaAreaTierExpression(), MAJOR_WATER_TIER_MAX],
29753
+ { "text-padding": MAJOR_WATER_TEXT_PADDING }
29754
+ );
29755
+ majorWaterLayers.push(...hoist);
29756
+ labelLayers.push(...watermark);
29757
+ continue;
29758
+ }
29759
+ labelLayers.push(...fanOrientationLabelByCellBand(label, bandFan, objectClass));
28907
29760
  }
28908
29761
  }
28909
29762
  }
@@ -28912,7 +29765,12 @@ function generateStyle(palette = "DAY", opts = {}) {
28912
29765
  (b) => b.label !== null && getLabelBehavior(b.objectClass) !== "orientation-cross-band"
28913
29766
  )
28914
29767
  );
28915
- for (const { label } of featureLabelBundles) {
29768
+ let majorsEmitted = majorWaterLayers.length === 0;
29769
+ for (const { label, objectClass: cls } of featureLabelBundles) {
29770
+ if (!majorsEmitted && getLabelPriority(cls) < MAJOR_WATER_LABEL_PRIORITY) {
29771
+ labelLayers.push(...majorWaterLayers);
29772
+ majorsEmitted = true;
29773
+ }
28916
29774
  if (bandFan === "collapsed") {
28917
29775
  labelLayers.push(makeCollapsedCrossBandLabel(label, isDominanceExempt(label)));
28918
29776
  continue;
@@ -28921,18 +29779,121 @@ function generateStyle(palette = "DAY", opts = {}) {
28921
29779
  labelLayers.push(...emitBand(label, code));
28922
29780
  }
28923
29781
  }
29782
+ if (!majorsEmitted) labelLayers.push(...majorWaterLayers);
28924
29783
  layers.push(...labelLayers);
28925
29784
  }
28926
29785
  const symbolSet = opts.symbolSet ?? "s52";
28927
29786
  const spriteBase = `${opts.tileBaseUrl ?? "https://tiles.chrt.co"}/v1/sprites`;
28928
29787
  const isLndareFill = (l) => /^s52-LNDARE-fill-band\d+$/.test(l.id);
28929
- const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
29788
+ const isWaterBaseFill = (l) => /^s52-(DEPARE|DRGARE|UNSARE)-fill-band\d+$/.test(l.id);
29789
+ const bandOf = (l) => {
29790
+ const m = /-band(\d+)$/.exec(l.id);
29791
+ return m ? Number(m[1]) : 0;
29792
+ };
28930
29793
  const emitted = opts.land ? (() => {
28931
- const lndareFills = layers.filter(isLndareFill);
28932
- const seaareFills = layers.filter(isSeaareFill);
28933
- const rest = layers.filter((l) => !isLndareFill(l) && !isSeaareFill(l));
28934
- return [rest[0], ...seaareFills, ...lndareFills, ...rest.slice(1)];
29794
+ const relocated = layers.filter((l) => isLndareFill(l) || isWaterBaseFill(l));
29795
+ const others = layers.filter((l) => !isLndareFill(l) && !isWaterBaseFill(l));
29796
+ const isSeaareFill = (l) => /^s52-SEAARE-fill-band\d+$/.test(l.id);
29797
+ const seaare = others.filter(isSeaareFill);
29798
+ const rest = others.filter((l) => !isSeaareFill(l));
29799
+ const isBaseLayer = (l) => {
29800
+ const id = l.id;
29801
+ if (id.startsWith("chrtbase-inland-")) return false;
29802
+ return id.startsWith("chrtbase-") || id === "s52-coverage-ground";
29803
+ };
29804
+ const baseBlock = rest.filter(isBaseLayer);
29805
+ const restAbove = rest.filter((l) => !isBaseLayer(l));
29806
+ const bands = [...new Set(relocated.map(bandOf))].sort((a, b) => a - b);
29807
+ const oneTan = landBaseColor(palette);
29808
+ const asBaseTan = (l) => ({
29809
+ ...l,
29810
+ paint: {
29811
+ ...l.paint ?? {},
29812
+ "fill-color": oneTan
29813
+ }
29814
+ });
29815
+ const ordered = [];
29816
+ for (const b of bands) {
29817
+ ordered.push(...relocated.filter((l) => bandOf(l) === b && isWaterBaseFill(l)));
29818
+ ordered.push(...relocated.filter((l) => bandOf(l) === b && isLndareFill(l)).map(asBaseTan));
29819
+ }
29820
+ const findRest = (id) => restAbove.findIndex((l) => l.id === id);
29821
+ const shoreAt = findRest("chrtbase-inland-shore");
29822
+ const pairAt = shoreAt !== -1 ? shoreAt : findRest("chrtbase-inland-hatch");
29823
+ let restFinal = restAbove;
29824
+ if (pairAt !== -1) {
29825
+ const repaint = [];
29826
+ for (const b of bands) {
29827
+ for (const l of relocated.filter((x) => bandOf(x) === b && isWaterBaseFill(x))) {
29828
+ const src = l;
29829
+ const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
29830
+ const notDom = {
29831
+ ...l,
29832
+ id: `${src.id}-overpair`,
29833
+ filter: ["all", src.filter, NOT_DOMINATED_FILTER],
29834
+ metadata: meta
29835
+ };
29836
+ delete notDom.maxzoom;
29837
+ delete notDom.minzoom;
29838
+ repaint.push(notDom);
29839
+ for (const D of BAND_DRAW_ORDER) {
29840
+ if (parseInt(D, 10) <= b) continue;
29841
+ const dMaxzoom = dominatedVariantMaxzoom(D);
29842
+ if (dMaxzoom <= 0) continue;
29843
+ const domVariant = {
29844
+ ...l,
29845
+ id: `${src.id}-overpair-domBy${D}`,
29846
+ filter: ["all", src.filter, dominatedByMinFilter(D)],
29847
+ metadata: meta,
29848
+ maxzoom: dMaxzoom
29849
+ };
29850
+ delete domVariant.minzoom;
29851
+ repaint.push(domVariant);
29852
+ }
29853
+ }
29854
+ }
29855
+ const isCoalneLine = (l) => /^s52-COALNE-line-band\d(-domBy\d)?$/.test(l.id);
29856
+ for (const l of layers.filter(isCoalneLine)) {
29857
+ const src = l;
29858
+ const meta = { ...src.metadata ?? {}, "chrt:role": "charted-water-repaint" };
29859
+ const banded = {
29860
+ ...l,
29861
+ id: `${src.id}-overpair`,
29862
+ metadata: meta,
29863
+ minzoom: 9
29864
+ };
29865
+ repaint.push(banded);
29866
+ const terminal = {
29867
+ ...l,
29868
+ id: `${src.id}-overpair-terminal`,
29869
+ filter: ["all", src.filter, ["==", ["get", "_terminal_shore"], true]],
29870
+ metadata: meta
29871
+ };
29872
+ delete terminal.minzoom;
29873
+ terminal.maxzoom = 9;
29874
+ repaint.push(terminal);
29875
+ }
29876
+ restFinal = [
29877
+ ...restAbove.slice(0, pairAt + 1),
29878
+ ...repaint,
29879
+ ...restAbove.slice(pairAt + 1)
29880
+ ];
29881
+ }
29882
+ return [restFinal[0], ...baseBlock, ...seaare, ...ordered, ...restFinal.slice(1)];
28935
29883
  })() : layers;
29884
+ for (const id of [
29885
+ "chrtbase-country-boundary",
29886
+ "chrtland-town-port",
29887
+ "chrtland-city-major"
29888
+ ]) {
29889
+ const at = emitted.findIndex(
29890
+ (l) => l.id === id
29891
+ );
29892
+ if (at !== -1) {
29893
+ const [layer] = emitted.splice(at, 1);
29894
+ emitted.push(layer);
29895
+ }
29896
+ }
28936
29897
  const style = {
28937
29898
  version: 8,
28938
29899
  name: `CHRT ${SYMBOL_SET_LABELS[symbolSet]} ${PALETTE_NAMES[palette]}${opts.chartMode ? ` \u2014 chart:${opts.chartMode.band}` : ""}`,
@@ -28944,6 +29905,7 @@ function generateStyle(palette = "DAY", opts = {}) {
28944
29905
  "chrt:layer-count": emitted.length,
28945
29906
  "chrt:land": Boolean(opts.land),
28946
29907
  "chrt:relief": Boolean(opts.relief),
29908
+ "chrt:base": Boolean(opts.base),
28947
29909
  "chrt:render-mode": opts.chartMode ? `chart:${opts.chartMode.band}` : "continuous",
28948
29910
  "chrt:description": "IHO S-52 portrayal rules for Electronic Navigational Charts (ENCs). Generated from the CHRT atlas engine."
28949
29911
  },
@@ -29437,6 +30399,7 @@ var Chrt = class {
29437
30399
  interact;
29438
30400
  featuresEp;
29439
30401
  searchEp;
30402
+ poiEp;
29440
30403
  queryEp;
29441
30404
  http;
29442
30405
  tileAuth;
@@ -29458,6 +30421,7 @@ var Chrt = class {
29458
30421
  this.weather = new WeatherEndpoint(this.http);
29459
30422
  this.featuresEp = new FeaturesEndpoint(this.http);
29460
30423
  this.searchEp = new SearchEndpoint(this.http);
30424
+ this.poiEp = new PoiEndpoint(this.http);
29461
30425
  this.queryEp = new QueryEndpoint(this.http);
29462
30426
  this.offline = new OfflineEndpoint(this.http, config.deviceId, config.manifestPath);
29463
30427
  this.interact = new InteractClient(this.http);
@@ -29493,6 +30457,26 @@ var Chrt = class {
29493
30457
  async search(options) {
29494
30458
  return this.searchEp.find(options);
29495
30459
  }
30460
+ /**
30461
+ * CHRTpoi — charted marine facilities in a viewport or radius.
30462
+ *
30463
+ * v1 serves marinas, derived from the ENC's own HRBFAC facility class —
30464
+ * the chart's data, with the source cell's currency inline per POI.
30465
+ *
30466
+ * ```ts
30467
+ * const { pois, coverage } = await chrt.pois({ bbox: [-70.7, 41.4, -70.5, 41.6] });
30468
+ * ```
30469
+ *
30470
+ * **Read `coverage.state` before interpreting an empty list.** `covered` +
30471
+ * empty means no marinas are charted there — a real answer. `none` means
30472
+ * CHRT cannot answer for that area at all, and an empty list means nothing.
30473
+ * The two must never be shown the same way, and `coverage.message` is
30474
+ * written to be relayed unedited. Metered on its own allowance
30475
+ * (`poi_called`), separate from features and search; not gated by tier.
30476
+ */
30477
+ async pois(options) {
30478
+ return this.poiEp.list(options);
30479
+ }
29496
30480
  /** Spatial proximity query around a point */
29497
30481
  async query(options) {
29498
30482
  return this.queryEp.nearby(options);
@@ -29663,6 +30647,18 @@ var Chrt = class {
29663
30647
  return generateStyle(palette, {
29664
30648
  region: this.region,
29665
30649
  tileBaseUrl: this.tileBaseUrl,
30650
+ // Pin each source's tile URL to the build it serves, from whatever the
30651
+ // token cache already holds. Without a discriminator one URL is shared
30652
+ // by every build, so after a cutover a map keeps painting already-fetched
30653
+ // tiles from the OLD build beside newly-fetched ones from the new.
30654
+ //
30655
+ // BEFORE the spread, so an explicit `buildTags` wins — and only ever a
30656
+ // warm-cache read: this method is synchronous and must not mint. A cold
30657
+ // cache yields `{}`, i.e. exactly the URLs this SDK emitted before the
30658
+ // discriminator existed. Warm it with `chrt.transformRequest()` (which
30659
+ // kicks off a mint) or `await chrt.styleUrl()`; `await chrt.warmTiles()`
30660
+ // is the explicit form, and it returns the map.
30661
+ buildTags: this.tileAuth.peekBuildTags(this.region),
29666
30662
  ...styleOpts,
29667
30663
  // The vessel's safety depth IS the style's safety contour — same number, same
29668
30664
  // meaning. Threading it here is what stops the chart shading at one depth while
@@ -29736,9 +30732,21 @@ var Chrt = class {
29736
30732
  * Pre-mint the tile token for a region. Optional — `transformRequest()` warms
29737
30733
  * the cache automatically — but useful if you want to surface a mint error
29738
30734
  * (e.g. `region_not_licensed`, `tile_quota_exceeded`) before the map loads.
30735
+ *
30736
+ * Returns what it minted, so a caller building its own sources can take the
30737
+ * `buildTags` map straight from here:
30738
+ *
30739
+ * ```ts
30740
+ * const { buildTags } = await chrt.warmTiles();
30741
+ * map.addSource("chrt", buildSources({ region: "NOAA_ENC", buildTags })["NOAA_ENC"]);
30742
+ * ```
30743
+ *
30744
+ * That pins each tile URL to the build it serves, so a build cutover gets its
30745
+ * own client cache namespace instead of leaving old and new tiles mixed on
30746
+ * screen. `chrt.style()` reads the same cache and needs no argument.
29739
30747
  */
29740
30748
  async warmTiles(region) {
29741
- await this.tileAuth.getToken(region);
30749
+ return this.tileAuth.getToken(region);
29742
30750
  }
29743
30751
  };
29744
30752
  export {
@@ -29748,9 +30756,13 @@ export {
29748
30756
  ATON_EXPRESSION_CLASSES,
29749
30757
  BAND_DRAW_ORDER,
29750
30758
  BAND_PRIORITY_RANGES,
30759
+ BASE_MAXZOOM,
30760
+ BASE_MINZOOM,
30761
+ BASE_REGION,
29751
30762
  BRIDGE_QUERY_LAYERS,
29752
30763
  BUILD_MAXZOOM,
29753
30764
  BUILD_MINZOOM,
30765
+ BUILD_PARAM,
29754
30766
  CHART_BAND_CODES,
29755
30767
  CHART_CASCADE_CODES,
29756
30768
  CHART_SECTIONS,
@@ -29932,6 +30944,7 @@ export {
29932
30944
  scaleSizeValue,
29933
30945
  scaminToMinZoom,
29934
30946
  styleModeBand,
30947
+ tileTemplate,
29935
30948
  withinBboxFilter,
29936
30949
  worldMinusBboxPolygon
29937
30950
  };