@tileflow/core 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,34 +1,1153 @@
1
1
  # @tileflow/core
2
2
 
3
- Typed Tileflow config helpers and the MapLibre style compiler.
3
+ Typed Tileflow map contracts, semantic modules, and deterministic MapLibre style compilation.
4
+
5
+ Every public `tileflow.config.ts` exports one map. Most maps import an existing map and override only
6
+ the design fields they own:
7
+
8
+ ```ts
9
+ import {
10
+ defineMap,
11
+ defineTheme,
12
+ fixed,
13
+ labels,
14
+ poi,
15
+ roads,
16
+ token,
17
+ water,
18
+ zoom,
19
+ } from '@tileflow/core';
20
+ import {streets, streetsThemes} from '@tileflow/maps';
21
+
22
+ const madridDark = defineTheme(streetsThemes.dark, {
23
+ id: 'madrid-dark',
24
+ version: 1,
25
+ colorScheme: 'dark',
26
+ tokens: {color: {'surface.land': '#0d1320', 'surface.water': '#081e2e'}},
27
+ });
28
+
29
+ export default defineMap({
30
+ id: 'madrid',
31
+ name: 'Madrid',
32
+ version: 1,
33
+ extends: streets,
34
+ themes: {light: streetsThemes.light, dark: madridDark},
35
+ defaultTheme: 'light',
36
+ systemThemes: {light: 'light', dark: 'dark'},
37
+ projection: 'globe',
38
+ modules: {
39
+ water: water({
40
+ bodies: {
41
+ fill: {
42
+ opacity: fixed(0.95, {reason: 'Madrid keeps this water density in every theme'}),
43
+ },
44
+ },
45
+ }),
46
+ roads: roads({
47
+ detail: 'streets',
48
+ hierarchy: 'strong',
49
+ classes: {
50
+ primary: {
51
+ surface: {
52
+ fill: {
53
+ color: fixed('#E4A85B', {reason: 'Madrid brand roads stay warm in every theme'}),
54
+ width: zoom.linear([
55
+ [7, fixed(0.6, {reason: 'Madrid keeps its primary-road hierarchy across themes'})],
56
+ [16, fixed(8, {reason: 'Madrid keeps its primary-road hierarchy across themes'})],
57
+ ]),
58
+ },
59
+ },
60
+ },
61
+ secondary: {surface: {fill: {color: token.color('roads.secondary')}}},
62
+ },
63
+ }),
64
+ labels: labels({
65
+ language: 'local',
66
+ places: 'all',
67
+ roads: 'streets',
68
+ styles: {
69
+ places: {
70
+ city: {
71
+ text: {
72
+ size: fixed(18, {reason: 'Madrid keeps its city-label scale across themes'}),
73
+ haloWidth: fixed(1.5, {reason: 'Madrid keeps its city-label halo across themes'}),
74
+ },
75
+ },
76
+ },
77
+ },
78
+ }),
79
+ poi: poi({categories: ['food-drink', 'arts-entertainment', 'transport'], color: 'category'}),
80
+ },
81
+ view: {center: [-3.7038, 40.4168], pitch: 35, zoom: 12},
82
+ });
83
+ ```
84
+
85
+ Map identity, scenes, and delivery policy belong to the leaf and do not inherit. Hosting policy such
86
+ as allowed browser origins is delivery metadata because it does not change cartographic output.
87
+
88
+ Set `projection: 'globe'` for MapLibre's adaptive globe preset. Global zooms render as a sphere,
89
+ then transition to Mercator between zoom 10 and 12 so detailed streets remain planar. Omit the
90
+ property, or set it to `'mercator'`, for a consistently flat map.
91
+
92
+ Terrain keeps the original `'none' | 'hillshade' | '3d'` shorthand. The object form can style every
93
+ MapLibre hillshade paint control, or generate vector contours in the browser from an explicit DEM
94
+ tile template. Contours require `demUrl`, `demMaxZoom`, and zoom-indexed `[minor, index]`
95
+ `thresholds`; the raster `url` remains the separate TileJSON endpoint. Set `mode: 'none'` to compile
96
+ contours without adding a raster source, hillshade, or 3D terrain:
97
+
98
+ ```ts
99
+ terrain: {
100
+ mode: 'none',
101
+ encoding: 'terrarium',
102
+ contours: {
103
+ demUrl: 'https://terrain.example.test/{z}/{x}/{y}.webp',
104
+ demMaxZoom: 13,
105
+ maxZoom: 15,
106
+ overzoom: 2,
107
+ thresholds: {
108
+ 9: [100, 500],
109
+ 11: [50, 250],
110
+ 13: [20, 100],
111
+ 15: [10, 50],
112
+ },
113
+ minor: {
114
+ color: token.color('terrain.contour.minor'),
115
+ width: token.number('terrain.contour.minor-width'),
116
+ },
117
+ index: {
118
+ color: token.color('terrain.contour.index'),
119
+ width: token.number('terrain.contour.index-width'),
120
+ },
121
+ labels: {
122
+ color: token.color('terrain.contour.label'),
123
+ haloColor: token.color('terrain.contour.halo'),
124
+ },
125
+ },
126
+ }
127
+ ```
128
+
129
+ Declare those `terrain.*` color and number roles with the same keys in every map theme. The
130
+ compiled source is ordinary MapLibre vector data with source layer `contours`; its tile URL
131
+ contains the safely encoded DEM template and complete generation parameters. Register the generic
132
+ protocol before MapLibre reads the style. It lazily initializes the pinned, locally bundled
133
+ `maplibre-contour@0.1.0` browser module on the first contour tile; no public CDN runtime is needed.
134
+ `overzoom` may not exceed the lowest threshold zoom, so generated DEM requests never use a negative
135
+ zoom. Every index interval must be a whole multiple of its minor interval. Contour labels emit the
136
+ scaled numeric elevation without assuming a display unit. Source and layer visibility cannot begin
137
+ before the first configured threshold zoom. To bound main-thread work, effective minor intervals
138
+ must be at least 250 units at z0–4, 100 at z5–7, 50 at z8–10, 20 at z11–12, and 10 from z13 onward.
139
+ Use a trusted, size-bounded DEM tile service; the protocol intentionally accepts author-supplied
140
+ HTTP(S) templates rather than proxying or republishing terrain data.
141
+
142
+ ```ts
143
+ import {registerTileflowContourProtocol} from '@tileflow/core/browser';
144
+
145
+ registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
146
+ ```
147
+
148
+ ## Authoring model
149
+
150
+ Tileflow exposes one authoring concept: a map. `defineRootMap()` creates a complete compiler root;
151
+ `defineMap()` creates an ordinary map whose `extends` value is another imported map object. All
152
+ eight first-party maps are independent roots. They use the semantic Streets compiler while defining
153
+ their complete designs and asset providers directly; no official map imports or extends another
154
+ official map. Applications can extend any of those roots through the same public API. Streets
155
+ itself owns coordinated light and dark themes.
156
+ There is no separate public recipe selector or compatibility alias.
157
+
158
+ The serialized root literal `compiler: 'streets'` is the current compiler-family ABI identifier,
159
+ retained for compatibility. It selects Core's semantic map engine; it does not select or import the
160
+ official `streets` map, and it does not supply that map's modules, icons, fonts, or other assets.
161
+ `compilerVersion` versions this ABI independently from map and package versions.
162
+
163
+ Custom build tooling can opt into a separate compiler-inspection sidecar from the build-only
164
+ entrypoint:
165
+
166
+ ```ts
167
+ import {createStyleWithInspection} from '@tileflow/core/build';
168
+
169
+ const {style, inspection} = createStyleWithInspection(map);
170
+ ```
171
+
172
+ `inspection.layers` stays aligned with the final Style layer order and records the semantic
173
+ `owner`, `slot`, `target`, and ordered add/patch effects that contributed to each layer, including
174
+ layers merged by the optimizer. `createStyleFromCatalogWithInspection` and
175
+ `createStylesFromCatalogWithInspection` provide the corresponding internal catalog forms. The
176
+ ordinary Style bytes are identical to compilation without inspection; private compiler metadata is
177
+ stripped before finalization, and the sidecar never enters a Style, runtime manifest, or production
178
+ artifact unless a caller deliberately stores it.
179
+
180
+ Resolution happens before validation, asset preparation, compilation, capture, build, or deploy.
181
+ Only `view` deep-merges (its nested arrays and expressions still replace). The `themes` collection
182
+ replaces atomically when declared and clears an inherited `systemThemes` mapping; omission inherits
183
+ the collection. `defaultTheme` may independently choose one inherited concrete theme, while an
184
+ explicit `systemThemes` replaces the complete light/dark mapping. Every resolved selector must name
185
+ a theme in the final collection.
186
+ `modules` merges by domain name: an omitted domain remains inherited, while declaring `roads(...)`
187
+ or another domain replaces that inherited module request and every compiler-owned contribution
188
+ attached to it as a unit. `data`, `projection`, `terrain`, `marine`, `icons`, and the text provider
189
+ are atomic; identity and tooling metadata are leaf-owned. `enabled: false` removes the domain and
190
+ all of its inherited effects. The resolved result is a standalone map with no runtime dependency on
191
+ TypeScript imports.
192
+
193
+ `icons` is an intentional exception to implicit array composition: omission inherits the exact
194
+ parent array, any declaration replaces it atomically, and `[]` disables map icons. Compose with a
195
+ spread when the child should keep a parent's directories. `fonts` and `glyphs` are mutually
196
+ exclusive text providers; declaring either atomically replaces an inherited provider of either
197
+ kind. Unknown keys and former compatibility shapes are rejected.
198
+
199
+ Most authors should extend an existing root. `defineRootMap()` is reserved for defining a complete
200
+ compiler-owned lineage:
201
+
202
+ ```ts
203
+ import {defineMap, defineRootMap, tileflowStreetsCompilerVersion} from '@tileflow/core';
204
+ import {streetsIcons, streetsThemes} from '@tileflow/maps';
205
+
206
+ const companyRoot = defineRootMap({
207
+ id: 'company-root',
208
+ name: 'Company root',
209
+ version: 1,
210
+ root: {compiler: 'streets', compilerVersion: tileflowStreetsCompilerVersion},
211
+ glyphs: {
212
+ kind: 'url',
213
+ url: 'https://api.tileflow.dev/fonts/{fontstack}/{range}.pbf',
214
+ fontStacks: ['Noto Sans Regular', 'Noto Sans Bold'],
215
+ },
216
+ icons: [streetsIcons],
217
+ themes: {light: streetsThemes.light, dark: streetsThemes.dark},
218
+ defaultTheme: 'light',
219
+ systemThemes: {light: 'light', dark: 'dark'},
220
+ });
221
+
222
+ export default defineMap({
223
+ id: 'company-navigation',
224
+ version: 1,
225
+ extends: companyRoot,
226
+ projection: 'globe',
227
+ });
228
+ ```
229
+
230
+ The root above is complete as written. A map always owns its complete text provider; Core does not
231
+ obtain fonts or sprites from World and never invents a fallback URL. The URL-backed first-party
232
+ `streets`, `ferraris`, `harad`, `soundings`, and `verdant` roots declare their glyph providers
233
+ directly, while `siegfried` declares packaged fonts, so ordinary imports and derived maps compile
234
+ without out-of-band release metadata.
235
+
236
+ ```mermaid
237
+ flowchart LR
238
+ A["tileflow.config.ts exports one map"] --> B["Resolve imported map lineage"]
239
+ B --> C["Complete design + domain compilers"]
240
+ C --> D["Ordered Streets layers"]
241
+ D --> E["MapLibre Style JSON"]
242
+ ```
243
+
244
+ Modules are keyed by domain, so object order never controls rendering and a domain can appear only
245
+ once. A map extending Streets inherits its complete module set. Use `enabled: false` to replace and
246
+ remove a domain deliberately. The public domains are `land`, `water`, `roads`, `buildings`, `boundaries`, `labels`,
247
+ `poi`, `aeroways`, `transit`, `vegetation`, `addresses`, and `landforms`.
248
+ The typed default recipe is exhaustive over that domain set, and a schema contract test verifies
249
+ that every recipe entry keeps its key, type tag, validator, compiler orchestration, and root export
250
+ in lockstep. Disabling `roads` also removes dependent road names, shields, and junction references;
251
+ independent place, water, aerodrome, and POI labels remain under the labels module.
252
+
253
+ Every styling module supports semantic shortcuts and exact semantic targets. Visual style values
254
+ accept typed token refs, documented `fixed(value, {reason})` values, raw MapLibre expressions through
255
+ `expression(...)`, and zoom functions through `zoom.step(...)`, `zoom.linear(...)`, or
256
+ `zoom.exponential(...)`. Refs and fixed leaves also work inside expressions and zoom stops.
257
+ Structural controls such as presets, visibility, and zoom gates remain ordinary literals. Exact
258
+ controls address stable concepts such as `roads.classes.primary.surface.fill`, not renderer layer
259
+ IDs.
260
+
261
+ The `land` module exposes stable land-use targets for `cemetery`, `civic`, `commercial`,
262
+ `education`, `government`, `industrial`, `medical`, `military`, `parking`, `railway`,
263
+ `recreation`, and `residential`. Its land-cover taxonomy distinguishes physical cover from authored
264
+ urban green: `farmland`, `flowerbed`, `grass`, `ice`, `meadow`, `protected`,
265
+ `recreationGround`, `rock`, `sand`, `scrub`, `urbanPark`, `villageGreen`, `wetland`, and
266
+ `wood`. The plain `grass` branch excludes every typed grass subclass, so one source feature cannot
267
+ receive two opaque green fills. Parking areas are polygons from the configured land-use source;
268
+ parking access aisles remain road features under `roads.serviceTypes.parkingAisle`. The
269
+ `commercial` target recognizes ordinary `commercial` and `retail` values plus Tileflow's derived
270
+ `business_area` ground class. Its `globalLandcover` fill styles the optional low-zoom global
271
+ land-cover extension without a raw layer patch. The `water.bathymetry` fill does the same for the
272
+ typed Tileflow World V1 depth bands. An explicit `water({bathymetryContours: {}})` traces the
273
+ submerged edges between those discrete polygon bands; it is an approximate visual contour, not a
274
+ surveyed isoline. `water({bathymetryLabels: {}})` separately opts into numeric metre values derived
275
+ from each polygon's absolute minimum band depth. Existing maps gain neither detail by default, and
276
+ sources without bathymetry bindings omit both requested layers. These labels describe coarse band
277
+ floors, not measured survey soundings, and must not be presented as hydrographic sounding data.
278
+
279
+ `addresses` renders the standard OpenMapTiles `housenumber` points at detailed zooms and exposes a
280
+ single semantic `labels` style. `landforms` renders the standard `mountain_peak` classes (`peak`,
281
+ `volcano`, `saddle`, `ridge`, `cliff`, and `arete`) with rank-aware collision priority and optional
282
+ metric elevation. Both source-layer and field names remain remappable through `openMapTiles(...)`;
283
+ landform names share the language selected by `labels({language: ...})`.
284
+
285
+ `aeroways.runwayRef` owns high-zoom runway designators from the remappable OpenMapTiles `ref`
286
+ field. Aerodrome names stay under `labels.styles.aerodrome`; `labels.aerodromeCodes` controls whether
287
+ they append no code, IATA only, or IATA with an ICAO fallback (`'none' | 'iata' | 'all'`).
288
+
289
+ The `vegetation` module binds individual-tree points from the optional OpenMapTiles `tree`
290
+ extension. Its default `mode: '3d'` emits a portable circle fallback plus metadata for runtimes
291
+ that can upgrade the points to instanced 3D trees. `mode: 'flat'` keeps the MapLibre circles, and
292
+ `flat` exposes the complete `CircleStyle` fallback. `threeDimensional` controls bark color,
293
+ broadleaf and conifer palettes, and independent height and crown scales. The legacy `minZoom`
294
+ shortcut remains available; `flat.minZoom` takes precedence when both are present. The binding
295
+ includes height, crown diameter, genus, leaf type, and species fields so compatible runtimes can
296
+ preserve source measurements and botanical form without hard-coding raw property names. The
297
+ pitched-scene stack follows physical height: pedestrian and transport surfaces, transport markings
298
+ and road names, buildings, then vegetation. Place, water, aerodrome, and POI annotations remain
299
+ last so geographic names stay readable without making street paint or text float over 3D geometry.
300
+
301
+ ## Shared visual primitives
302
+
303
+ The same small visual language is reused across every geographic domain. This keeps an agent from
304
+ having to learn different spellings for the same MapLibre behavior:
305
+
306
+ - `BackgroundStyle`: color, opacity, pattern, visibility, and zoom range.
307
+ - `FillStyle`: color, opacity, pattern, antialiasing, visibility, and zoom range.
308
+ - `LineStyle`: color, width, opacity, dash, blur, gap, offset, pattern, caps, joins, and zoom range.
309
+ - `LineHatchStyle`: repeated diagonal detail with color, opacity, spacing, size, angle, and zoom
310
+ range for a road structure.
311
+ - `TextStyle`: field, exact font face, exact fallback faces, size, color, halo, spacing metrics, transform,
312
+ collision policy, rotation, fixed/variable anchoring, line constraints, and zoom range.
313
+ - `IconStyle`: image, size, color, halo, rotation/alignment, collision policy, and zoom range.
314
+ - `CircleStyle`: radius, fill/stroke appearance, blur, opacity, pitch behavior, and zoom range.
315
+ - `ExtrusionStyle`: color, height, base, opacity, pattern, vertical gradient, and zoom range.
316
+
317
+ Compounds express common cartographic structures: `AreaStyle` has `fill` and `outline`,
318
+ `LineStackStyle` has `shadow`, `casing`, and `fill`, and `SymbolStyle` combines placement with
319
+ optional `text`, `icon`, and `marker`. Geographic selection remains owned by semantic modules;
320
+ ordinary visual styles intentionally do not accept raw source filters.
321
+
322
+ ## Compiled-style performance
323
+
324
+ The Streets compiler resolves semantic modules and their owned contributions first, then compacts equivalent
325
+ physical MapLibre layers. At high-detail zooms, road classes become a small set of data-driven
326
+ cohorts; equivalent road labels and tunnel hatches share compatible buckets; and land-cover,
327
+ land-use, and waterway classes share compatible buckets. Compaction selects
328
+ cohorts from compiler-owned semantic targets, not by parsing physical IDs; its temporary
329
+ owner/slot/target provenance is stripped from the public Style JSON.
330
+
331
+ After optimization, Core embeds a bounded private `tileflow:interaction-manifest` lookup for the
332
+ final POI representations. `@tileflow/interactions/maplibre` validates and consumes that metadata
333
+ so applications can bind to `domain: 'poi'` without depending on physical layer IDs. The lookup is
334
+ paired atomically with the exact style it describes; it is not a public style-authoring API.
335
+
336
+ Use the exported structural sweep to enforce budgets without loading tiles or a browser:
4
337
 
5
338
  ```ts
6
- import {defineTileflow, osm, roads} from '@tileflow/core';
339
+ import {analyzeTileflowStylePerformance, createStyle} from '@tileflow/core';
340
+ import map from './tileflow.config';
7
341
 
8
- export default defineTileflow({
9
- themes: {
10
- light: {
11
- mode: 'light',
12
- colors: {
13
- water: '#8ED6E8',
14
- park: '#C3F1D5',
15
- road: '#D8E0E7',
16
- text: '#566371',
342
+ const report = analyzeTileflowStylePerformance(createStyle(map));
343
+ console.log(report.zooms[16]);
344
+ ```
345
+
346
+ The report covers z0–z22 by default and includes active layers, conservative bucket estimates,
347
+ symbols, and active layers per source-layer. It is intended for regression gates; browser traces
348
+ remain the source of truth for decode, upload, placement, and frame time.
349
+
350
+ A `SymbolStyle` root zoom range governs its text/icon layer and is inherited by its marker layer.
351
+ Because a marker is materialized as a separate circle layer, an explicit marker range may refine
352
+ that inherited default. Text and icon ranges must agree because MapLibre renders them together.
353
+
354
+ The road module distinguishes the path family semantically:
355
+
356
+ ```ts
357
+ roads({
358
+ extras: {paths: true},
359
+ crossings: {image: token.image('roads.crosswalk')},
360
+ sidewalks: {
361
+ surface: {color: token.color('roads.city.casing'), minZoom: 17},
362
+ pattern: {
363
+ pattern: token.image('roads.sidewalkPattern'),
364
+ minZoom: 17,
365
+ opacity: fixed(0.6, {reason: 'Sidewalk texture strength is invariant'}),
366
+ },
367
+ },
368
+ roundabouts: {
369
+ casing: {strokeColor: token.color('roads.default.casing')},
370
+ fill: {strokeColor: token.color('roads.default')},
371
+ },
372
+ areas: {
373
+ pedestrian: {
374
+ fill: {color: token.color('surface.land')},
375
+ outline: {
376
+ color: token.color('roads.default.casing'),
377
+ width: fixed(1, {reason: 'Pedestrian-area outline weight is invariant'}),
378
+ },
379
+ },
380
+ },
381
+ classes: {
382
+ primary: {
383
+ tunnel: {
384
+ casing: {
385
+ color: token.color('roads.city.tunnelCasing'),
386
+ width: fixed(10, {reason: 'Primary-tunnel hierarchy is invariant'}),
387
+ },
388
+ fill: {
389
+ color: token.color('roads.city.tunnel'),
390
+ width: fixed(8, {reason: 'Primary-tunnel hierarchy is invariant'}),
391
+ },
392
+ hatch: {
393
+ color: token.color('roads.city.tunnelCasing'),
394
+ opacity: fixed(0.25, {reason: 'Tunnel hatch strength is invariant'}),
395
+ spacing: fixed(10, {reason: 'Tunnel hatch rhythm is invariant'}),
396
+ },
397
+ },
398
+ },
399
+ pedestrian: {
400
+ surface: {
401
+ fill: {
402
+ color: token.color('roads.path.transition'),
403
+ width: fixed(6, {reason: 'Pedestrian-road hierarchy is invariant'}),
404
+ },
405
+ },
406
+ },
407
+ footway: {
408
+ surface: {
409
+ fill: {
410
+ color: token.color('roads.path'),
411
+ width: fixed(1.2, {reason: 'Path hierarchy is invariant'}),
412
+ },
413
+ },
414
+ },
415
+ cycleway: {
416
+ surface: {
417
+ fill: {
418
+ color: token.color('roads.cycleway'),
419
+ width: fixed(1.5, {reason: 'Cycleway hierarchy is invariant'}),
420
+ },
421
+ },
422
+ },
423
+ steps: {
424
+ surface: {
425
+ fill: {
426
+ color: token.color('roads.path.casing'),
427
+ width: fixed(1.4, {reason: 'Step hierarchy is invariant'}),
428
+ },
429
+ },
430
+ },
431
+ pathway: {
432
+ surface: {
433
+ fill: {
434
+ color: token.color('roads.path'),
435
+ width: fixed(1.2, {reason: 'Path hierarchy is invariant'}),
436
+ },
17
437
  },
18
438
  },
19
439
  },
20
- maps: {
21
- madrid: {
22
- basemap: osm(),
23
- theme: 'light',
24
- modules: [roads({preset: 'modern', density: 'balanced'})],
25
- view: {
26
- center: [-3.7038, 40.4168],
27
- zoom: 12,
440
+ modifiers: {
441
+ expressway: {widthScale: 1.06},
442
+ ramp: {widthScale: 0.7},
443
+ unpaved: {surface: {fill: {color: token.color('roads.path')}}},
444
+ construction: {
445
+ surface: {
446
+ fill: {opacity: fixed(0.7, {reason: 'Construction emphasis is invariant'})},
28
447
  },
29
448
  },
449
+ indoor: {
450
+ surface: {fill: {opacity: fixed(0.4, {reason: 'Indoor-road emphasis is invariant'})}},
451
+ },
452
+ },
453
+ restrictions: {
454
+ access: {
455
+ surface: {fill: {opacity: fixed(0.55, {reason: 'Restricted-road emphasis is invariant'})}},
456
+ },
457
+ toll: {
458
+ surface: {
459
+ casing: {color: fixed('#C5B7D8', {reason: 'Toll ink is regulatory and invariant'})},
460
+ },
461
+ },
462
+ },
463
+ serviceTypes: {
464
+ driveway: {widthScale: 0.75},
465
+ parkingAisle: {widthScale: 0.6},
30
466
  },
31
467
  });
32
468
  ```
33
469
 
34
- Docs: https://tileflow.dev/docs
470
+ These targets are non-overlapping translations of the OpenMapTiles road class and subclass. The
471
+ same names are available under `labels().roadClasses` and `labels().styles.roads`. Surface, tunnel,
472
+ and bridge phases can be controlled independently for each target; each structure has
473
+ `shadow`/`casing`/`fill` and an optional `hatch`. Glyph hatch marks inherit the resolved fill width
474
+ when `size` is omitted and never participate in label collision. Setting `hatch.pattern` instead
475
+ emits a repeated sprite texture clipped to the resolved fill width. No raw source filter or generated
476
+ layer ID is needed. `hatch.patternWidths` can list intrinsic sprite heights named
477
+ `${pattern}-${width}`; the compiler selects the nearest height from the resolved road width so the
478
+ texture's marks remain approximately constant in screen pixels across classes, ramps, and zooms.
479
+
480
+ `classes.pedestrian` styles line-like pedestrian ways. Polygon pedestrian plazas are a distinct
481
+ geometry and use `areas.pedestrian`, including optional fill opacity, outline color, or sprite
482
+ pattern. This prevents plazas from degrading into a thin polygon outline.
483
+
484
+ Road treatments refine a class without creating another class or exposing source fields.
485
+ `construction`, `expressway`, `indoor`, `official`, `ramp`, and `unpaved` live under `modifiers`;
486
+ general, bicycle, foot, horse, and toll restrictions live under `restrictions`; `alley`,
487
+ `crossover`, `driveway`, `parkingAisle`, and `yard` live under `serviceTypes`; and exact
488
+ mountain-bike difficulty values live under `mountainBike`. Treatments reuse
489
+ surface/tunnel/bridge and casing/fill/shadow paint controls, plus a relative `widthScale`. The
490
+ compiler emits feature-driven paint expressions inside the existing stable class layers rather
491
+ than multiplying every possible combination.
492
+
493
+ Road names, shields, and motorway junctions remain label concerns:
494
+
495
+ ```ts
496
+ labels({
497
+ roads: 'all',
498
+ shields: 'major',
499
+ junctions: true,
500
+ styles: {
501
+ shields: {
502
+ default: {
503
+ icon: {
504
+ image: token.image('roads.shield.rectangleNeutral'),
505
+ optional: false,
506
+ pitchAlignment: 'viewport',
507
+ rotationAlignment: 'viewport',
508
+ textFit: 'width',
509
+ textFitPadding: [0, 4, 0, 4],
510
+ },
511
+ text: {
512
+ color: token.color('labels.shieldDark'),
513
+ font: token.font('places'),
514
+ optional: false,
515
+ pitchAlignment: 'viewport',
516
+ rotationAlignment: 'viewport',
517
+ },
518
+ },
519
+ overview: {minZoom: 6, maxZoom: 11, placement: 'point'},
520
+ detail: {minZoom: 11, placement: 'line', spacing: 400},
521
+ kinds: {
522
+ 'rectangle-neutral': {image: token.image('roads.shield.rectangleNeutral')},
523
+ 'rectangle-blue': {image: token.image('roads.shield.rectangleBlue')},
524
+ },
525
+ textColors: {
526
+ dark: {color: token.color('labels.shieldDark')},
527
+ light: {color: token.color('labels.shieldLight')},
528
+ },
529
+ },
530
+ junctions: {
531
+ text: {
532
+ color: token.color('labels.strong'),
533
+ haloColor: token.color('labels.halo'),
534
+ haloWidth: fixed(2, {reason: 'Junction halo weight is invariant'}),
535
+ },
536
+ },
537
+ },
538
+ });
539
+ ```
540
+
541
+ `icon.textFit` and `icon.textFitPadding` let a symbol background grow with its text. Setting both
542
+ icon and text `optional: false` keeps a route shield atomic during collision placement: neither an
543
+ empty badge nor a detached reference can render alone. `overview` renders producer-selected points
544
+ at low zoom and `detail` switches to line placement at street zooms; exclusive `maxZoom: 11` and
545
+ inclusive `minZoom: 11` make that handoff atomic. Keeping both icon and text aligned to `viewport`
546
+ makes the badge horizontal regardless of road bearing.
547
+
548
+ `kinds` and `textColors` are closed presentation tables. The vector producer supplies semantic
549
+ `shield_kind`, `shield_text_color`, `shield_text`, and `shield_rank` values; Core only matches them
550
+ against the authored tables and falls back to `default`. Country, route-prefix, and network-shape
551
+ rules therefore stay in the data authority instead of leaking into a map theme or multiplying
552
+ runtime layers.
553
+
554
+ Tileflow World owns POI classification, editorial zoom eligibility, and cross-source ranking. Its
555
+ canonical `poi` layer exposes `category`, `type`, `icon`, `min_zoom`, `filter_rank`, and `size_rank`;
556
+ Core validates that complete contract and never reconstructs those decisions from OpenMapTiles
557
+ `class`, `subclass`, or `rank`. `filter_rank` is an
558
+ integer from 0 through 5, with lower values reserved for the strongest candidates. The numeric
559
+ `density` option is an inclusive threshold from 1 through 5 and defaults to 3. `size_rank` is an
560
+ integer from 0 through 16 used to order eligible candidates before MapLibre performs final
561
+ collision placement.
562
+
563
+ The closed category vocabulary is `arts-entertainment`, `education`, `food-drink`, `landmark`,
564
+ `lodging`, `medical`, `park-nature`, `public-services`, `religion`, `retail`, `sport-leisure`,
565
+ `transport`, and `visitor-amenity`. Producer `type` and `icon` values use stable snake_case names.
566
+ When an `icon` sprite exists it is used directly; an unknown or absent icon safely falls back to the
567
+ category's themed image role. `labels` and `icons` are booleans. Category styles control only
568
+ presentation and may hide or delay candidates, never promote them above the producer's selection.
569
+ `placement.coupleIconAndLabel` keeps a POI's icon and label atomic during collision placement.
570
+
571
+ ```ts
572
+ poi({
573
+ categories: ['food-drink', 'landmark', 'transport'],
574
+ density: 3,
575
+ icons: true,
576
+ labels: true,
577
+ });
578
+ ```
579
+
580
+ The same contract drives regular POI layers, custom HUD treatments, semantic interactions, and
581
+ feature inspection. There is intentionally no compatibility path for the former class mapping,
582
+ rank ceilings, or named density/detail presets.
583
+
584
+ ## Themes and module styles
585
+
586
+ A map declares complete named appearances in `themes`, chooses one deterministic
587
+ `defaultTheme`, and may map the browser's light/dark preference through `systemThemes`. `system` is
588
+ never a compiled theme name: it is a runtime request that resolves immediately to one concrete
589
+ name. Disk builds write `styles/<map>/<theme>.json` beneath their output root. Public and
590
+ content-addressed Style URLs belong to the runtime manifest and may include a deployment prefix;
591
+ capture scenes and receipts store the concrete name rather than `system`.
592
+
593
+ Themes are flat, JSON-safe documents with `colorScheme`, identity, typography, lighting, and four
594
+ typed token catalogs: `color`, `font`, `image`, and `number`. Every theme on one map must expose the
595
+ same token keys, so changing a theme can never change map structure or silently lose a semantic
596
+ role. `defineTheme(base, definition)` is an authoring convenience that returns a fully materialized
597
+ document; it does not leave an inheritance edge in the resolved config.
598
+
599
+ The key in the `themes` record is its concrete runtime selector and output-path segment. The
600
+ theme's own `id` and `version` are its editorial identity and build provenance, so they need not
601
+ equal that key. Concrete keys may be names such as `day` and `night`; every `systemThemes.light` or
602
+ `systemThemes.dark` value must name an existing theme whose `colorScheme` matches that branch.
603
+
604
+ ```ts
605
+ import {color, defineMap, defineTheme, fixed, roads, token} from '@tileflow/core';
606
+ import {streets, streetsThemes} from '@tileflow/maps';
607
+
608
+ const dark = defineTheme(streetsThemes.dark, {
609
+ id: 'company-dark',
610
+ version: 1,
611
+ colorScheme: 'dark',
612
+ tokens: {
613
+ color: {
614
+ 'surface.land': '#101722',
615
+ 'labels.primary': color.mix('#eef3fb', token.color('surface.background'), {amount: 0.08}),
616
+ },
617
+ },
618
+ });
619
+
620
+ export default defineMap({
621
+ id: 'company',
622
+ version: 1,
623
+ extends: streets,
624
+ themes: {light: streetsThemes.light, dark},
625
+ defaultTheme: 'light',
626
+ systemThemes: {light: 'light', dark: 'dark'},
627
+ modules: {
628
+ roads: roads({
629
+ classes: {
630
+ primary: {
631
+ surface: {
632
+ fill: {
633
+ color: fixed('#ff2d78', {reason: 'Regulatory primary-road ink is invariant'}),
634
+ },
635
+ },
636
+ },
637
+ secondary: {
638
+ surface: {
639
+ fill: {
640
+ color: token.color('roads.secondary'),
641
+ },
642
+ },
643
+ },
644
+ },
645
+ }),
646
+ },
647
+ });
648
+ ```
649
+
650
+ Module recipes own structure and semantic behavior; visual fields accept typed token references.
651
+ Use a visual literal only inside a theme. If a module value intentionally must not vary, wrap it in
652
+ `fixed(value, {reason})`. `tileflow inspect --json` reports stable `THEME_IMPLICIT_FIXED`
653
+ diagnostics: implicit color, font, and image literals block Style compilation, while direct visual
654
+ number literals are warnings so an agent can document their invariance deliberately. Structural
655
+ numbers such as zoom bounds and ranks are not visual-theme diagnostics. `color.alpha()` and
656
+ deterministic OKLCH `color.mix()` keep derived palette logic inspectable. Unknown refs, cycles,
657
+ cross-category refs, token-schema drift, and unresolved visual nodes fail before Style JSON is
658
+ emitted.
659
+
660
+ There is no public physical-layer override surface. New cartographic behavior belongs to a typed
661
+ control in its owning module. Official maps may add compiler-private semantic contributions, but
662
+ each has exactly one module owner and is discarded atomically when that module is replaced or
663
+ disabled. `createStyle()` validates the final optimized Style JSON with the MapLibre style spec and
664
+ never returns an invalid style.
665
+
666
+ ## Data is separate from design
667
+
668
+ Omitting `data` selects the compiler-owned Tileflow World `v1` compatibility generation:
669
+
670
+ ```ts
671
+ import {streets} from '@tileflow/maps';
672
+
673
+ export default streets;
674
+ ```
675
+
676
+ Theme typography can set `fallbacks`, `letterSpacing`, and `transform` globally or per label
677
+ domain, while the font token catalog gives module styles a stable semantic target. Text delivery is
678
+ explicit and atomic: a map may declare either ordered `fonts` directories
679
+ for browser font files or a `glyphs` provider for PBF glyphs, never both. Omitting both inherits the
680
+ parent's provider; declaring either replaces an inherited provider of either kind. After resolution,
681
+ a map that emits text must have exactly one provider. A text-free root may omit both. On a derived
682
+ map, omission inherits the parent provider while `fonts: []` explicitly removes it; that empty array
683
+ is valid only when the resolved map emits no text.
684
+
685
+ ```ts
686
+ import {defineMap, defineTheme} from '@tileflow/core';
687
+ import {streets, streetsThemes} from '@tileflow/maps';
688
+
689
+ const brandLight = defineTheme(streetsThemes.light, {
690
+ id: 'brand-light',
691
+ version: 1,
692
+ colorScheme: 'light',
693
+ tokens: {font: {default: 'Brand Sans Regular', places: 'Brand Sans Regular'}},
694
+ });
695
+ const brandDark = defineTheme(streetsThemes.dark, {
696
+ id: 'brand-dark',
697
+ version: 1,
698
+ colorScheme: 'dark',
699
+ tokens: {font: {default: 'Brand Sans Regular', places: 'Brand Sans Regular'}},
700
+ });
701
+
702
+ export default defineMap({
703
+ id: 'brand-map',
704
+ version: 1,
705
+ extends: streets,
706
+ fonts: ['./fonts'],
707
+ themes: {light: brandLight, dark: brandDark},
708
+ defaultTheme: 'light',
709
+ systemThemes: {light: 'light', dark: 'dark'},
710
+ });
711
+ ```
712
+
713
+ Node preparation reads TTF, OTF, and WOFF2 files, uses each OpenType full name as its exact
714
+ `text-font` ID, and requires `LICENSE.txt` in every contributing directory. Directories apply left
715
+ to right; a later exact name replaces an earlier face and case-only collisions fail. Only primary
716
+ faces used by the final style become content-addressed assets and strict `tileflow:fontFaces`
717
+ metadata. `font` names an exact OpenType full name or glyph face; local `fallbacks` name exact faces
718
+ or explicit CSS generic families. Tileflow never appends a weight suffix or derives one face from
719
+ another. Browser adapters load local faces before MapLibre. Native releases use a locked PBF
720
+ `glyphs` URL provider instead, which enumerates the exact comma-joined MapLibre request keys in
721
+ `fontStacks`.
722
+
723
+ Streets, Ferraris, Härad, Soundings, and Verdant each declare
724
+ `https://api.tileflow.dev/fonts/{fontstack}/{range}.pbf` with the exact `Noto Sans Regular` and
725
+ `Noto Sans Bold` stacks. That compatibility URL is canonical but not content-addressed; responses revalidate and
726
+ do not make a resolved map byte-reproducible. Exact official glyph identity belongs to the
727
+ separately published `/base/<assetSetSha256>/glyphs/...` global base-asset contract. In that URL,
728
+ `assetSetSha256` identifies the standalone glyph collection; it is not the same-domain value as the
729
+ per-map `assetSetSha256` in `build-manifest.json`.
730
+ Cyberpunk and Matrix each replace the URL provider with their own packaged Oxanium directory and
731
+ reference the exact local faces `Oxanium Medium` and `Oxanium SemiBold`. Siegfried owns its packaged
732
+ Cormorant Garamond Regular, SemiBold, and Italic faces.
733
+
734
+ Name the official generation deliberately, or use another OpenMapTiles-compatible vector source:
735
+
736
+ ```ts
737
+ import {openMapTiles, tileflowWorld, vectorTiles} from '@tileflow/core';
738
+
739
+ const official = tileflowWorld();
740
+ const external = vectorTiles({
741
+ tiles: ['pmtiles://./test/fixtures/world.pmtiles'],
742
+ revision: 'fixture-1',
743
+ attribution: '© Example © OpenStreetMap contributors',
744
+ schema: openMapTiles(),
745
+ });
746
+ ```
747
+
748
+ The resolved map controls how data is drawn; `data` controls where compatible features come from.
749
+ For Tileflow World, the compiler emits the selector TileJSON URL
750
+ `https://api.tileflow.dev/tiles/world/tiles.json`. Omitted data or `tileflowWorld()` selects
751
+ `world-v1/current`; `tileflowWorld({release: {releaseId, descriptorSha256}})` binds an exact release
752
+ through query parameters. Runtime and capture resolve `current` once per session or job and then
753
+ use the immutable release described by that response. `openMapTiles({layers, fields})` can bind renamed
754
+ source-layers or properties while preserving the versioned semantic contract. External browser
755
+ credentials do not belong in public Style JSON; supply them through the framework's
756
+ `transformRequest` integration.
757
+
758
+ External exact-test sources may use one TileJSON `url` or a bounded direct `tiles` list with optional
759
+ `bounds`, `minzoom`, and `maxzoom`. `pmtiles://` is supported for a checked-in or bring-your-own
760
+ archive. Public vector URLs are HTTPS, root-relative, or HTTP only on loopback development hosts.
761
+ A PMTiles URL names one `.pmtiles` archive through an HTTPS, loopback HTTP, root-relative, or safe
762
+ repository-relative target; credentials, fragments, traversal, and other protocols are rejected at
763
+ both authoring validation and compilation. An external `revision` participates in capture identity,
764
+ so changing fixture bytes cannot silently reuse a baseline.
765
+
766
+ Tileflow's OpenMapTiles contract also recognizes the optional `globallandcover` extension. The
767
+ official Streets map maps its `barren`, `crop`, `grass`, `shrub`, `snow`, `trees`, and
768
+ `urban` classes beneath the OSM land layers. Tileflow World V1 supplies native generalized
769
+ geometry through z10; Streets fades it progressively while detailed `landcover`/`landuse` gains
770
+ opacity and removes it at z11. This is a macro bridge, not local semantic detail. Sources with a
771
+ different layer name can use `openMapTiles({layers: {globalLandcover: 'my_landcover'}})`; archives
772
+ without the extension remain compatible and simply render no features for that style layer. Use
773
+ `land({globalLandcover: {...}})` to customize its fill, opacity, visibility, and zoom range. `crop`
774
+ shares the theme's semantic `landcover.farmland` color with detailed farmland, while `barren` uses
775
+ `landcover.rock`; neither class borrows a road color.
776
+
777
+ The normalized schema also records the meaning of the OpenMapTiles `park` layer. Generic
778
+ `openMapTiles()` defaults to `semantics.parkLayer: 'mixed'` and compiles a private compatibility
779
+ branch for ordinary legacy parks. `tileflowWorldV1Schema()` fixes it to `'protected-only'`: that
780
+ layer is a protection tint, while urban parks and gardens come from
781
+ `landcover.class=grass/subclass=park|garden`. The ambiguous legacy `park` class is therefore not
782
+ a public land-cover target.
783
+
784
+ Tileflow World V1 makes `bathymetry`, `globallandcover`, `circular_feature`, `sidewalk`, and
785
+ `street_furniture` mandatory. Use `tileflowWorldV1Schema()` for that product instead of making raw
786
+ layer names part of a style. Publication tooling calls `validateTileflowWorldV1Tilejson(...)` and
787
+ fails closed unless each layer occurs exactly once, declares its contracted native zooms, and
788
+ advertises the typed fields consumed by the maps. This includes bathymetry z0–z9, global land cover
789
+ z0–z10, the three detailed-city layers at native z15, and numeric
790
+ `transportation.clearance_extra_px_z15` for butt-capping procedural-roundabout approaches. Generic
791
+ OpenMapTiles sources keep the detailed extensions optional and the compiler omits unsupported
792
+ detail. Resolving omitted data or
793
+ `tileflowWorld(...)` uses the strict V1 schema directly, and `water({bathymetry: {...}})` styles the
794
+ emitted depth bands without raw layer IDs. Use `water({bathymetryContours: {...}})` to customize the
795
+ opt-in companion line layer; an empty style selects subtle defaults, and the zero-depth band is
796
+ excluded so the ordinary coastline remains authoritative. The lines follow discrete band polygon
797
+ edges and are therefore approximate rather than surveyed depth contours. Use
798
+ `water({bathymetryLabels: {...}})` to customize the separate opt-in symbol layer; an empty style
799
+ selects the defaults, whose text is the absolute band-minimum number in metres with no unit suffix.
800
+ It remains a band-floor annotation, not a survey sounding.
801
+
802
+ For independent ocean products, `marine` composes stable auxiliary vector sources without changing
803
+ the primary World source:
804
+
805
+ ```ts
806
+ import {bathymetry, defineMap, fixed, nautical} from '@tileflow/core';
807
+ import {streets} from '@tileflow/maps';
808
+
809
+ export default defineMap({
810
+ id: 'chart',
811
+ version: 1,
812
+ extends: streets,
813
+ marine: 'chart', // 'none' | 'bathymetry' | 'nautical' | 'chart'
814
+ modules: {
815
+ nautical: nautical({
816
+ soundings: {minZoom: 13},
817
+ aids: {minZoom: 11},
818
+ lights: {minZoom: 12},
819
+ hazards: {minZoom: 12},
820
+ hazardAreas: {
821
+ fill: {opacity: fixed(0.1, {reason: 'Chart hazard wash is invariant'})},
822
+ },
823
+ wrecks: {minZoom: 12},
824
+ wreckAreas: {
825
+ outline: {opacity: fixed(0.6, {reason: 'Chart wreck outline is invariant'})},
826
+ },
827
+ reefs: {fill: {opacity: fixed(0.3, {reason: 'Chart reef wash is invariant'})}},
828
+ navigationAreas: {
829
+ fill: {opacity: fixed(0.05, {reason: 'Chart navigation-area wash is invariant'})},
830
+ },
831
+ coverage: {outline: {visible: false}},
832
+ labels: {
833
+ navigationAreas: {minZoom: 11},
834
+ reefs: {minZoom: 13},
835
+ },
836
+ }),
837
+ },
838
+ });
839
+ ```
840
+
841
+ `bathymetry` resolves to vector source `tileflow-bathymetry` and supplies the 13-stop
842
+ `bathymetry-v1` band contract to `water(...)`. `nautical` resolves to `tileflow-nautical`; the
843
+ `nautical(...)` module
844
+ styles its `sounding`, `aid`, `light`, `hazard`, `wreck`, `reef`, `navigation_area`, and `coverage`
845
+ layers. The schema also names the product's provider-neutral provenance fields (`provenance`,
846
+ `provider`, `cell`, `edition`, `update`, `scale`, `coverage`, and `licence`). `chart` enables both.
847
+ Point features label depth, name, or light character as applicable. The `lighthouses` symbol style
848
+ targets `aid` points whose canonical `class` is `lighthouse`, leaving other aids under `aids`.
849
+ Polygon reefs, navigation
850
+ areas, hazards, wrecks, and named/provider coverage have separate collision-aware labels under
851
+ `nautical({labels: {...}})`; unnamed coverage does not emit empty text.
852
+ Core does not hard-code a nautical provider or attribution: the selected TileJSON owns its actual
853
+ coverage and attribution, and derived vector tiles are not represented as a certified ENC for
854
+ navigation. The V1 geometry contract preserves `sounding`, `aid`, and `light` as points;
855
+ `reef`, `navigation_area`, and `coverage` as polygons; and both point and polygon geometries for
856
+ `hazard` and `wreck`. Point symbols and polygon areas compile separately, without fabricated
857
+ centroids.
858
+ Advanced callers can override either TileJSON URL, attribution, or source ID and can set the other
859
+ member to `false`:
860
+
861
+ ```ts
862
+ marine: {
863
+ bathymetry: {url: 'https://tiles.example.test/depth/tiles.json'},
864
+ nautical: false,
865
+ }
866
+ ```
867
+
868
+ Use the `bathymetry(...)` helper when the map needs relief or product-level styling. Bathymetry is
869
+ one logical product with two independently loaded physical sources:
870
+
871
+ ```ts
872
+ marine: {
873
+ bathymetry: bathymetry({
874
+ display: 'hybrid',
875
+ bands: {
876
+ opacity: fixed(0.72, {reason: 'Keep chart symbols above the depth wash'}),
877
+ },
878
+ contours: {visible: true},
879
+ relief: {
880
+ multidirectional: true,
881
+ opacity: fixed(0.18, {reason: 'Relief stays legible beneath bands and labels'}),
882
+ },
883
+ }),
884
+ nautical: false,
885
+ }
886
+ ```
887
+
888
+ `display: 'bands'` loads only vector TileJSON `/tiles/bathymetry/tiles.json` as
889
+ `tileflow-bathymetry`. `display: 'relief'` loads only `/tiles/bathymetry/dem/tiles.json` as
890
+ `tileflow-bathymetry-dem` and emits continuous `color-relief` plus hillshade. `display: 'hybrid'`
891
+ loads both. The hosted DEM contract is 512 px, Terrarium-encoded, lossless WebP; advanced compatible
892
+ sources may override `url`, `sourceId`, `encoding`, or `tileSize` inside `relief`.
893
+ Multidirectional illumination uses directions 270°, 315°, 0°, and 45°. The simple
894
+ `marine: 'chart'` shorthand deliberately stays vector-only; DEM relief is an explicit advanced
895
+ opt-in.
896
+
897
+ The vector palette uses thresholds −11 000, −8 000, −6 000, −4 000, −2 000, −1 000, −500, −200,
898
+ −100, −50, −20, −10, and 0 metres. `bands`, `contours`, and `labels` remain separate controls.
899
+ Until native contour fields are contracted, contours are a visual fallback drawn from band polygon
900
+ edges, not surveyed isolines. The V1 schema exposes optional source-layer names
901
+ `bathymetry_contour`, `seafloor_landform`, `water_name`, and `bathymetry_coverage`, but promises
902
+ fields only for required `bathymetry.min_depth` and `bathymetry.sort_key`.
903
+
904
+ The DEM enables future client-side sampling and depth profiles, but Core intentionally exposes no
905
+ profile or remote sampling API yet. Rendered relief must not be interpreted as navigation-safe
906
+ depth data.
907
+
908
+ Omitting `marine` preserves the transitional Tileflow World V1 bathymetry fallback. Declaring
909
+ `marine: 'none'` suppresses that fallback, while a selected bathymetry sidecar always takes
910
+ precedence. Compiled styles retain the singular `tileflow:data` metadata for existing consumers and
911
+ add `tileflow:sources` plus `tileflow:sourceRequirements` for independent product validation.
912
+
913
+ Detailed city datasets can bind `sidewalk`, `streetFurniture`, and `circularFeature` source layers
914
+ through `openMapTiles({layers, fields})`. When present, `roads.sidewalks` owns source-backed
915
+ pedestrian polygons, `roads.crossings` owns oriented crossing icons, and `roads.roundabouts` owns
916
+ metric circular road rings. These bindings are optional: a generic OpenMapTiles source remains
917
+ valid and the compiler omits unsupported detail instead of inventing geometry. Crossing icons are
918
+ explicit because the data contract cannot assume a sprite name. They require positive physical
919
+ `markings` evidence and reject `crossing=no`; both fields are semantic bindings, so custom schemas
920
+ can remap them without weakening that rule.
921
+
922
+ The optional `business_corridor` extension contains activity-selected source footprints below
923
+ buildings. Its `activity_score`, `rank`, `min_zoom`, and `confidence` fields control a quiet local
924
+ warm tint without drawing POI-radius circles. Current building footprints may carry sparse
925
+ `building_tone=commercial|destination|active`; absence means the neutral base building. The three
926
+ values preserve why the warm tone was selected even when a theme maps them to the same color.
927
+ Building visibility is fixed by the layer zoom contract and never by optional semantics. Immutable
928
+ V8.9 archives carrying `building_kind` or `has_business` remain readable as a defensive
929
+ compatibility path, but new candidates do not emit those fields. The land module also exposes
930
+ `medical`, `education`, and `government` independently instead of collapsing their already distinct
931
+ source classes into one civic color.
932
+
933
+ World and text assets are independent contracts. `tileflowWorld()` selects `world-v1/current` or an
934
+ exact `releaseId + descriptorSha256`; a `glyphs` declaration contains its own complete URL. Ordinary
935
+ imports of Streets, Ferraris, Härad, Siegfried, Soundings, Cyberpunk, Matrix, and
936
+ Verdant remain usable because each official map owns or inherits a URL or packaged-font provider.
937
+ URL-backed maps become exact-byte reproducible
938
+ when the immutable global base-asset set is published and their explicit URL is updated to its
939
+ `assetSetSha256` path; no World or compiler fallback participates in that rollout. The global
940
+ base-asset manifest's hash and a map build manifest's identically named `assetSetSha256` use
941
+ different contracts and must never be substituted for one another.
942
+
943
+ Upstream data attribution remains in the MapLibre source. `Map by Tileflow` is a separate product
944
+ credit/trademark surface and must not replace, obscure, or be presented as upstream attribution.
945
+
946
+ ## Capture scenes
947
+
948
+ Commit named `scenes` on the singular exported map. A scene implicitly targets that map, so it does
949
+ not repeat a `map` selector. Scene metadata does not inherit and is removed before cartographic
950
+ compilation; it affects visual evidence, not style or manifest identity.
951
+
952
+ ```ts
953
+ import {defineMap} from '@tileflow/core';
954
+ import {streets} from '@tileflow/maps';
955
+
956
+ export default defineMap({
957
+ id: 'madrid',
958
+ name: 'Madrid',
959
+ version: 1,
960
+ extends: streets,
961
+ scenes: {
962
+ 'madrid-desktop': {
963
+ theme: 'dark',
964
+ camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12},
965
+ viewport: {width: 1280, height: 800, dpr: 1},
966
+ },
967
+ },
968
+ });
969
+ ```
970
+
971
+ Use `tileflow preview --scene madrid-desktop` (`tileflow dev` remains an alias) for live review and
972
+ `tileflow capture madrid-desktop` for exact pixels and a schema-version-4 receipt containing the
973
+ concrete theme plus Streets, data, style, renderer, and image identities. World capture resolves the selected TileJSON
974
+ once and records the exact `world-v1` release plus descriptor/archive/data-contract hashes;
975
+ external fixtures may identify their explicit revision.
976
+
977
+ Every exact World V1 release ID is already canonical at the producer boundary: 12–128 characters
978
+ matching `^world-v1-[a-z0-9][a-z0-9._-]*[a-z0-9]$`. Core validates the literal value and never trims,
979
+ lowercases, or upgrades it to another World generation.
980
+
981
+ ## Public API and browser subpath
982
+
983
+ Config, data, modules, compilation, validation, runtime resolution, and capture-scene APIs remain
984
+ available from the package root. Capture scenes, manifests, and browser-runtime surfaces also have
985
+ explicit `@tileflow/core/capture`, `@tileflow/core/manifest`, and `@tileflow/core/runtime`
986
+ subpaths. Node build integrations import the small deterministic map-identity contract from
987
+ `@tileflow/core/build`: `collectTileflowMapBuildLineage`, `createTileflowMapBuildManifest`,
988
+ `hashTileflowMapRevision`, `hashTileflowAssetSet`, and `hashTileflowAssetSetIdentities`. The latter
989
+ reproduces the same asset-set v1 hash from exact per-file identities after another system has
990
+ independently confirmed the immutable bytes; package or bundle hashes are not substitutes. The
991
+ previous aggregate-wrapper and recipe-selector APIs, `renderer`, top-level `tiles`/`tileset`, module
992
+ arrays, and raw `layers` are not part of this API.
993
+
994
+ `@tileflow/core/recipe` is nevertheless a real exported ABI used by `@tileflow/maps` to declare
995
+ owner-scoped semantic effects for first-party recipes. It is a low-level compiler integration
996
+ surface, not a recipe selector and not the ordinary application authoring API; application maps
997
+ should use typed modules from the package root. Because Maps imports this subpath from its installed
998
+ Core peer, incompatible changes to it participate in Core SemVer and package compatibility checks.
999
+
1000
+ `mapRevisionSha256` hashes only resolved cartography: the effective design after `extends`, the
1001
+ compiler family, compiler-owned effective contributions, and exact source icon/font identities.
1002
+ Leaf `id`, `name`, editorial `mapVersion`, default `view`, capture `scenes`, and `delivery` policy do
1003
+ not change that content identity. They remain explicit on the map, manifest, Style, capture receipt,
1004
+ or Hosted deployment fingerprint that owns them. Compiler ABI, package versions, generated assets,
1005
+ and the concrete release selected by World `current` likewise have separate identities.
1006
+
1007
+ Runtime manifest version 1 has one shape for local and Hosted delivery. Every map entry owns
1008
+ `defaultTheme`, optional `systemThemes`, and a `themes` record whose leaves contain
1009
+ `colorScheme`, `styleUrl`, and optional font/deployment identity. Hosted-only identity is additive
1010
+ metadata on the same map entry; there is no delivery discriminator or parallel `styles` alias.
1011
+ `parseTileflowRuntimeManifest()` accepts only this canonical shape. The strict bounded schema uses
1012
+ the same portable map and theme IDs as authoring, rejects `system` as a published name, duplicate
1013
+ font identities, unsafe owner-relative URLs, prototype-bearing input, unknown fields, and JSON
1014
+ larger than 1 MiB.
1015
+
1016
+ ```json
1017
+ {
1018
+ "version": 1,
1019
+ "maps": {
1020
+ "madrid": {
1021
+ "defaultTheme": "light",
1022
+ "systemThemes": {"light": "light", "dark": "dark"},
1023
+ "themes": {
1024
+ "light": {"colorScheme": "light", "styleUrl": "/styles/madrid/light.json"},
1025
+ "dark": {"colorScheme": "dark", "styleUrl": "/styles/madrid/dark.json"}
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+ ```
1031
+
1032
+ Runtime fetches share a successful result for 30 seconds, never cache failures, use
1033
+ `cache: 'no-store'`, enforce a 10-second timeout (configurable up to 60 seconds), compose an
1034
+ external abort signal, and can be invalidated with `clearTileflowManifestCache()`.
1035
+
1036
+ Named map `view` values travel in the manifest. `resolveTileflowRuntimeTheme()` turns an omitted,
1037
+ explicit, or browser `system` request into one concrete published entry; unknown names fail closed.
1038
+ `resolveTileflowRuntimeView()` defines the shared
1039
+ precedence as explicit runtime values, then the manifest view, then the single exported
1040
+ `defaultTileflowRuntimeView` (`[0, 20]`, zoom 2, bearing/pitch 0). Browser delivery is one
1041
+ discriminated `TileflowRuntimeSource`: `kind: 'tileflow'` resolves a named map only through its
1042
+ published manifest, while `kind: 'maplibre'` accepts a direct style object or URL. The runtime
1043
+ subpath does not import the config compiler, invent a localhost style URL, or change image mode by
1044
+ environment.
1045
+
1046
+ Framework adapters import the browser-only lifecycle kernel explicitly:
1047
+
1048
+ ```ts
1049
+ import {
1050
+ attachTileflowFairUseNotice,
1051
+ attachTileflowMapLifecycle,
1052
+ createTileflowMarkerController,
1053
+ createTileflowSessionStarter,
1054
+ createTileflowTransformRequest,
1055
+ registerTileflowWorldRequestBridge,
1056
+ } from '@tileflow/core/browser';
1057
+ ```
1058
+
1059
+ The browser entry is not re-exported from the root. It is SSR-safe to import, has no MapLibre
1060
+ runtime dependency, and reads no browser global during module evaluation. See the
1061
+ [framework browser runtime contract](../../docs/contracts/framework-browser-runtime.md) and the
1062
+ [cartographic authoring contract](../../docs/contracts/cartographic-authoring.md). Exact field
1063
+ resolution and asset rules live in the
1064
+ [map inheritance contract](../../docs/contracts/map-inheritance.md).
1065
+
1066
+ ## Hosted session authorization
1067
+
1068
+ The framework adapters use `createTileflowSessionController` to obtain one short-lived session
1069
+ grant before eligible hosted style, tile, font, or sprite requests. Concurrent resources share the
1070
+ same preflight, grants are attached only to reviewed HTTP(S) origins, and an expired unused session
1071
+ rotates once when the server requests it. A controller also rotates after six hours or 10,000
1072
+ eligible requests. The preflight has a 10-second client timeout by default; direct controller users
1073
+ can set `grantTimeoutMs` from 1 to 120,000 milliseconds.
1074
+
1075
+ `analytics.enabled: false` disables only the optional analytics beacon. It does not disable this
1076
+ server-owned commercial authorization. User `transformRequest` callbacks still run first; Tileflow
1077
+ then decorates only the resulting eligible URL and preserves the other request options.
1078
+
1079
+ For exact Tileflow World release tiles, framework adapters also use the browser request bridge to
1080
+ observe the response's safe fair-use state without adding identity, query parameters, credentials,
1081
+ or user headers to the public World URL. The bridge requires the immutable release path and exactly
1082
+ one lowercase descriptor digest; the retired mutable World template is never intercepted. Early
1083
+ `GRACE` stays silent; signed late `GRACE` creates a compact
1084
+ accessible owner-action pill, and `CLAIM_REQUIRED` creates a stronger in-map banner. A missing
1085
+ header, absent tile, MapLibre error, or failed response cannot erase an existing claim action; a
1086
+ later successful `OPEN` response can clear it. Shaped empty tiles remain render-safe while the owner
1087
+ action stays available. Successful World tile bodies are streamed with a 16 MiB maximum: an
1088
+ oversized `Content-Length` is rejected before reading, and chunked responses are cancelled as soon
1089
+ as their accumulated bytes cross the same bound. Abort signals cancel an active reader; errors do
1090
+ not include the remote response body.
1091
+
1092
+ Other subpaths under `src/`, `themes/`, `templates/`, and `modules/` are internal implementation
1093
+ details and can change during alpha releases.
1094
+
1095
+ Official map definitions and the source assets they own ship together in `@tileflow/maps`, not in
1096
+ Core. That package exports the maps and their reusable directory descriptors:
1097
+
1098
+ ```ts
1099
+ import {
1100
+ cyberpunk,
1101
+ cyberpunkFonts,
1102
+ cyberpunkIcons,
1103
+ ferraris,
1104
+ ferrarisIcons,
1105
+ harad,
1106
+ haradIcons,
1107
+ matrix,
1108
+ matrixFonts,
1109
+ matrixIcons,
1110
+ siegfried,
1111
+ siegfriedFonts,
1112
+ siegfriedIcons,
1113
+ soundings,
1114
+ soundingsIcons,
1115
+ streets,
1116
+ streetsIcons,
1117
+ streetsThemes,
1118
+ verdant,
1119
+ verdantIcons,
1120
+ } from '@tileflow/maps';
1121
+ ```
1122
+
1123
+ All eight official maps are complete compiler roots. Each selects the semantic Streets compiler
1124
+ without importing or extending another official map and declares its own icon directory.
1125
+ Streets declares `[streetsIcons]` and exposes its complete coordinated appearances as
1126
+ `streetsThemes.light` and `streetsThemes.dark`; image tokens select the matching sidewalk pattern
1127
+ without changing the asset collection. Cyberpunk declares `[cyberpunkIcons]` and
1128
+ `[cyberpunkFonts]`; Matrix independently declares `[matrixIcons]` and `[matrixFonts]`. The same
1129
+ asset operation is available to applications:
1130
+
1131
+ ```ts
1132
+ export default defineMap({
1133
+ id: 'brand-map',
1134
+ version: 1,
1135
+ extends: streets,
1136
+ icons: [...streets.icons, './icons'],
1137
+ });
1138
+ ```
1139
+
1140
+ `icons` is a `readonly TileflowIconDirectory[]`. Omission inherits the parent's exact array,
1141
+ declaration replaces it atomically, and `[]` means no icons. Directories apply left to right.
1142
+ `<id>.<ext>` publishes an icon as `<id>`; `<id>.pattern.<ext>` publishes an intrinsic-size pattern
1143
+ as `<id>`. The published ID must already be canonical lower-kebab; a later exact ID wins and a
1144
+ case-only collision fails. There is no built-in selector, source object, external
1145
+ sprite selector, icon mapping, icon-specific inheritance, additive command, or compatibility alias.
1146
+
1147
+ `@tileflow/dev` resolves local and package directory descriptors, verifies real-path containment,
1148
+ and prepares ordinary public artifacts without serializing installation paths. It compiles one
1149
+ deterministic sprite from the final icon composition and validates every literal `icon-image`,
1150
+ `fill-pattern`, and `line-pattern` in the final style against it. It also prepares any declared font
1151
+ directories generically; `cyberpunkFonts`, `matrixFonts`, and `siegfriedFonts` are ordinary package
1152
+ descriptors rather than pipeline special cases. Calling the pure compiler for a map whose style needs unprepared
1153
+ assets fails instead of emitting broken runtime references.