@kahwee/sf-map-svg 0.3.0 → 1.0.0

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +23 -4
  3. package/README.md +213 -2
  4. package/SOURCES.md +4 -0
  5. package/{data → dist/data}/README.md +1 -0
  6. package/dist/data/bart-stations.json +134 -0
  7. package/dist/data/catalog.json +4718 -0
  8. package/dist/data/coast.json +36 -0
  9. package/dist/data/districts-2002.json +325 -0
  10. package/dist/data/districts-2012.json +325 -0
  11. package/dist/data/districts-2022.json +325 -0
  12. package/dist/data/highways.json +4138 -0
  13. package/dist/data/index.d.ts +26 -0
  14. package/{data → dist/data}/index.js +37 -39
  15. package/dist/data/key-roads.json +12662 -0
  16. package/dist/data/landmarks.json +167 -0
  17. package/dist/data/neighborhoods-analysis.json +817 -0
  18. package/dist/data/neighborhoods-realtor.json +1978 -0
  19. package/dist/data/neighborhoods.json +2267 -0
  20. package/dist/data/types.d.ts +117 -0
  21. package/dist/data/types.js +1 -0
  22. package/dist/src/data.d.ts +25 -0
  23. package/{src → dist/src}/data.js +17 -20
  24. package/dist/src/explorer-layout.d.ts +23 -0
  25. package/dist/src/explorer-layout.js +72 -0
  26. package/dist/src/explorer.d.ts +4 -0
  27. package/dist/src/explorer.js +955 -0
  28. package/dist/src/geometry.d.ts +5 -0
  29. package/dist/src/geometry.js +53 -0
  30. package/dist/src/immutable.d.ts +2 -0
  31. package/dist/src/immutable.js +9 -0
  32. package/dist/src/index.d.ts +13 -0
  33. package/dist/src/index.js +125 -0
  34. package/dist/src/interactive.d.ts +4 -0
  35. package/dist/src/interactive.js +5 -0
  36. package/dist/src/layers.d.ts +36 -0
  37. package/dist/src/layers.js +64 -0
  38. package/dist/src/navigation.d.ts +6 -0
  39. package/dist/src/navigation.js +145 -0
  40. package/dist/src/overlays.d.ts +25 -0
  41. package/dist/src/overlays.js +15 -0
  42. package/dist/src/svg.d.ts +4 -0
  43. package/dist/src/svg.js +7 -0
  44. package/dist/src/types.d.ts +108 -0
  45. package/dist/src/types.js +1 -0
  46. package/dist/src/viewport.d.ts +5 -0
  47. package/dist/src/viewport.js +27 -0
  48. package/package.json +34 -24
  49. package/data/bart-stations.json +0 -134
  50. package/data/catalog.json +0 -4699
  51. package/data/coast.json +0 -36
  52. package/data/districts-2002.json +0 -325
  53. package/data/districts-2012.json +0 -325
  54. package/data/districts-2022.json +0 -325
  55. package/data/highways.json +0 -4138
  56. package/data/index.d.ts +0 -117
  57. package/data/landmarks.json +0 -167
  58. package/data/neighborhoods-analysis.json +0 -817
  59. package/data/neighborhoods-realtor.json +0 -1978
  60. package/data/neighborhoods.json +0 -2267
  61. package/src/geometry.d.ts +0 -8
  62. package/src/geometry.js +0 -51
  63. package/src/immutable.js +0 -8
  64. package/src/index.d.ts +0 -51
  65. package/src/index.js +0 -122
  66. package/src/layers.js +0 -54
  67. package/src/overlays.js +0 -18
  68. package/src/svg.js +0 -12
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ - Publish the stable 1.0 API with public npm access and a public source repository.
6
+ - Add the reusable interactive map entrypoint, controlled viewport and selection APIs, keyboard/touch navigation, and marker selection.
7
+ - Add interactive examples, Storybook stories, and viewport/navigation regression checks.
8
+
9
+ - Add district/neighborhood explorer modes and a labels toggle, with fixed screen-size labels while zooming.
10
+ - Add a master visible-label switch for standalone SVGs.
11
+
12
+ - Add nine optional key-road landmarks from DataSF centerlines, public JSON, and zoom-aware explorer labels.
13
+
14
+ - Add a transit-inspired map theme with pale water, quiet land, green parks, and blue station symbols.
15
+ - Refine the neighborhood explorer presentation and map legend.
16
+ - Add a transit example and Storybook preset.
17
+
18
+ ## 0.4.1
19
+
20
+ - Migrate library source to strict TypeScript 7 with generated JavaScript and declarations.
21
+ - Replace Prettier with Biome formatting, import organization, and recommended lint rules.
22
+
23
+ ## 0.4.0
24
+
25
+ - Add a browser neighborhood explorer with alias search, selection, boundary zoom, and GeoJSON downloads.
26
+ - Adapt explorer labels to zoom and viewport size while preserving static SVG defaults.
27
+ - License software under MIT and add npm release automation and packaged-consumer checks.
28
+
3
29
  ## 0.3.0
4
30
 
5
31
  - Enable public npm distribution with explicit public registry access.
package/LICENSE CHANGED
@@ -1,5 +1,24 @@
1
- Copyright (c) 2026 KahWee Teng. All rights reserved.
1
+ MIT License
2
2
 
3
- Public availability does not grant a license to redistribute or modify the software.
4
- The geographic data comes from DataSF and remains subject to its source terms;
5
- see SOURCES.md. Those third-party terms are not replaced by this notice.
3
+ Copyright (c) 2026 KahWee Teng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ Geographic data retains its source terms and attribution requirements;
24
+ see SOURCES.md. This software license does not replace those terms.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![District fills, optional neighborhood boundaries, and plain outlines](docs/map-preview.png)
4
4
 
5
- Private package extracted from KahWee’s **San Francisco District Map** Site. Draws a self-contained SVG with bundled geometry and no runtime dependencies, tiles, WebGL, or network requests.
5
+ An MIT-licensed package extracted from KahWee’s **San Francisco District Map** Site. Draws a self-contained SVG with bundled geometry and no runtime dependencies, tiles, WebGL, or network requests.
6
6
 
7
7
  ```js
8
8
  import { renderSFMap, createSFMap } from '@kahwee/sf-map-svg';
@@ -27,7 +27,9 @@ Write `svg` to a `.svg` file or embed it in your page. For Astro, render it with
27
27
  | `year` | `2022` | District boundaries: `2002`, `2012`, or `2022` |
28
28
  | `districtLines` | `true` | Supervisorial district outlines |
29
29
  | `neighborhoodLines` | `false` | Dashed SFAR realtor neighborhood outlines |
30
+ | `theme` | `'districts'` | Use `'transit'` for pale blue water, ivory land, green parks, and blue BART symbols; custom `colors` still take precedence |
30
31
  | `districtFills` | `true` | Original Site’s eleven muted district colors |
32
+ | `labels` | `true` | Master switch for visible map text; symbols and accessible titles remain |
31
33
  | `districtLabels` | `true` | District number badges |
32
34
  | `highways` | `false` | Original Site’s highway geometry |
33
35
  | `landmarks` | `false` | Golden Gate Park, Presidio, Lincoln Park, Twin Peaks, Dolores Park, and McLaren Park |
@@ -35,7 +37,8 @@ Write `svg` to a `.svg` file or embed it in your page. For Astro, render it with
35
37
  | `width`, `height` | `800`, `800` | SVG viewBox and intrinsic size |
36
38
  | `padding` | `28` | Space around the coast |
37
39
  | `markers` | `[]` | Points with `id`, `lng`, `lat`, optional `label`, `color`, `selected` |
38
- | `colors` | Built-in palette | Override `water`, `land`, `district`, `neighborhood`, `highway`, `park`, `landmark`, `bart`, `label`, `marker`, `selected` |
40
+ | `keyRoads` | `false` | Nine selected road corridors and names for orientation |
41
+ | `colors` | Built-in palette | Override `water`, `land`, `district`, `neighborhood`, `highway`, `road`, `park`, `landmark`, `bart`, `label`, `marker`, `selected` |
39
42
  | `title` | `San Francisco map` | Accessible SVG title |
40
43
  | `idPrefix` | Unique per process | Set explicitly for deterministic output or independent server renders |
41
44
 
@@ -63,6 +66,8 @@ Open `examples/generated/index.html` to compare district and neighborhood maps.
63
66
 
64
67
  The package uses a small Mercator SVG renderer while retaining the Site’s boundary geometry, coastline, palette, district labels, and highway data.
65
68
 
69
+ The `theme: 'transit'` preset borrows the clear visual hierarchy of [BART’s system map](https://www.bart.gov/system-map), retaining geographic positions. It uses a quiet, single-color land fill instead of district colors. The neighborhood explorer uses this preset.
70
+
66
71
  Enable `landmarks`, `bartStations`, and `highways` together for the featured example. Park fills use `colors.park`, park labels use `colors.landmark`, and station rings and labels use `colors.bart`. These current geographic overlays are independent of the district year; BART stations are city-only (Daly City is outside the map). Station positions are geographic points, not a route diagram.
67
72
 
68
73
  ## Examples
@@ -131,3 +136,209 @@ const matchingDefinitions = searchNeighborhoods('mission');
131
136
  ```
132
137
 
133
138
  These are 250 **source-specific definitions**, not 250 distinct neighborhoods. Canonical names are package display names, and boundaries reflect each documented source rather than a claimed universal consensus. Mission and Outer Mission remain distinct. JSON files are the source of truth used by the renderer; the default map and lookup use the 92 SFAR realtor neighborhoods. See [the data API guide](data/README.md) for all filenames, schema, lookup rules, source comparisons, and custom SVG overlays. Storybook provides downloadable JSON files beside its neighborhood examples.
139
+
140
+ ## Interactive neighborhood explorer
141
+
142
+ The browser explorer includes canonical-name and alias search, source selection, neighborhood outlines, zoom controls, and GeoJSON downloads. SFAR realtor definitions are selected by default; SF Find and analysis neighborhoods remain separate choices.
143
+
144
+ ```js
145
+ import { createNeighborhoodExplorer } from '@kahwee/sf-map-svg/explorer';
146
+
147
+ const explorer = createNeighborhoodExplorer({ source: 'realtor' });
148
+ document.querySelector('#map').append(explorer);
149
+ explorer.selectNeighborhood('NoPa');
150
+
151
+ // Before removing the component, release its observers and event listeners.
152
+ // explorer.destroy();
153
+ ```
154
+
155
+ Call this browser-only factory after a DOM is available. Importing it does not mount anything. Options include `source`, an optional initial `neighborhood` name or alias, and district `year`. The returned element also exposes `setSource(source)`, `zoomBy(factor)`, and `resetView()`.
156
+
157
+ Selected downloads are one-feature GeoJSON FeatureCollections retaining source attribution and boundary-processing metadata.
158
+
159
+ At city scale, labels stay sparse. Zooming reveals neighborhood and BART names, with label sizing and collision checks based on the visible viewport. Station points remain visible. The static `renderSFMap` API keeps its existing labels and defaults.
160
+
161
+ Run `pnpm demo`, serve the repository root over HTTP, and open `examples/generated/explorer.html`. Storybook includes city, selected neighborhood, alternative-source, and mobile examples.
162
+
163
+ ## License
164
+
165
+ Software is licensed under MIT. Geographic datasets retain their source terms and attribution requirements; see [SOURCES.md](SOURCES.md).
166
+
167
+
168
+ ## TypeScript development
169
+
170
+ The library is authored in strict TypeScript 7. Run `pnpm build` to compile JavaScript and declarations into `dist/`. JavaScript consumers require no TypeScript runtime. `pnpm format` applies Biome formatting and safe lint fixes; `pnpm check` checks Biome, data, source and consumer types, and tests.
171
+
172
+ Enable `keyRoads: true` for Market, Mission, Geary, Van Ness, 19th Avenue, Sunset, The Embarcadero, Columbus, and Divisadero. These use active DataSF centerlines, not invented routes. The explorer reveals road names as you zoom. Import `keyRoads` from the data entry point or `data/key-roads.json` for geometry, source segment IDs, and label anchors.
173
+
174
+ ### Map modes and labels
175
+
176
+ The interactive explorer includes a map-mode selector and a Labels toggle. `mode: 'districts'` shows numbered supervisorial districts; `mode: 'neighborhoods'` shows names from the selected neighborhood source (SFAR realtor by default). Labels are collision-filtered and remain about 12 screen pixels through map zoom and resize; more names fit as you zoom in. Road labels use 11 pixels.
177
+
178
+ ```js
179
+ const explorer = createNeighborhoodExplorer({ mode: 'districts', labels: true });
180
+ explorer.setLabels(false);
181
+ explorer.setMode('neighborhoods');
182
+ explorer.setLabels(true);
183
+ ```
184
+
185
+ `renderSFMap({ labels: false })` also hides all visible text while retaining station symbols and accessible titles. Standalone SVGs are static images; the interactive explorer provides the constant-size labels during map zoom.
186
+
187
+ ## Reusable interactive map
188
+
189
+ The new `@kahwee/sf-map-svg/interactive` entry point provides a map, accessible controls, and
190
+ attribution without the explorer's search sidebar or detail panel. It does not change URLs,
191
+ load articles, apply editorial filters, or navigate. Importing the static entry point does not
192
+ import this interactive runtime. Both paths retain zero runtime dependencies.
193
+
194
+ ```js
195
+ import { createInteractiveSFMap } from '@kahwee/sf-map-svg/interactive';
196
+
197
+ const map = createInteractiveSFMap({
198
+ mode: 'neighborhoods',
199
+ source: 'analysis', // 'sf-find' and 'realtor' are also available
200
+ theme: 'transit',
201
+ labelSize: { min: 12, max: 15 },
202
+ layers: { districtFills: false, districtLines: false, districtLabels: false },
203
+ });
204
+ document.querySelector('#map').append(map);
205
+ map.addEventListener('neighborhoodchange', ({ detail }) => {
206
+ // On clear: id, name, and feature are null. source always identifies the dataset.
207
+ console.log(detail.id, detail.name, detail.source);
208
+ });
209
+ map.selectNeighborhood('Mission', { fit: false });
210
+ const selection = map.getSelection(); // { id, name, source, feature } or null
211
+ map.selectNeighborhood(null);
212
+ ```
213
+
214
+ `createInteractiveSFMap()` defaults to `mode: 'basemap'`: no district or neighborhood layers.
215
+ Parks, roads, and stations are initially enabled and can be disabled independently. The existing
216
+ `createNeighborhoodExplorer()` and static APIs retain their realtor/district defaults. The
217
+ source option only chooses a definition collection; it does not make that collection visible
218
+ in basemap mode. Editorial groupings belong to the consumer and are never treated as geographic
219
+ aliases. Import types including `InteractiveSFMapOptions`, `InteractiveSFMapElement`,
220
+ `MapViewport`, `MapPadding`, and `NeighborhoodSelection` from the interactive entry point.
221
+
222
+ | Option | Default | Behavior |
223
+ | --- | --- | --- |
224
+ | `mode` | `basemap` (interactive), `neighborhoods` (explorer) | `basemap`, `districts`, or `neighborhoods`; establishes layer defaults |
225
+ | `source` | `realtor` | `realtor`, `sf-find`, or `analysis`; explicit source recommended for reusable integrations |
226
+ | `theme` | `transit` | `transit` or `districts` |
227
+ | `year` | `2022` | District vintage: 2002, 2012, or 2022 |
228
+ | `layers` | Mode defaults | Independent booleans: `districtFills`, `districtLines`, `districtLabels`, `neighborhoodLines`, `neighborhoodLabels`, `landmarks`, `bartStations`, `highways`, `keyRoads` |
229
+ | `labels` | `true` | Master visible-text switch; accessible descriptions and station symbols remain |
230
+ | `labelSize` | `{ min: 11, max: 12 }` | Screen-pixel range, 8–32 allowed. Nominal sizes are 11 for roads and 12 otherwise, clamped to this range; sizes never grow with zoom |
231
+ | `selectableNeighborhoods` | `true` | Enables pointer/keyboard selection; false retains geography and labels |
232
+ | `neighborhood` | Unset | Initial name, alias, or ID in the selected source |
233
+ | `fitPadding` | `24` | Screen pixels, number or `{ top, right, bottom, left }`, used by selection and geometry fitting after mounting |
234
+ | `markers` | `[]` | Supplied `MapMarker` records with unique, nonempty IDs; no content fetching |
235
+ | `markerRadius` / `markerHitSize` | `6` / `44` | Screen-pixel visible radius and tap-target diameter, independent of zoom; selected radius grows by 2px |
236
+ | `markerColor` / `selectedMarkerColor` | `#245b61` / `#f04f32` | Default marker colors; individual `marker.color` overrides the unselected color |
237
+ | `onMarkerActivate` | Unset | Called when a non-null marker selection changes, including programmatic changes |
238
+
239
+ Explicit layer options override mode defaults even after `setMode()`. District fills, outlines,
240
+ and badges can therefore be composed with neighborhood names without requiring district
241
+ labels. Descriptions identify only displayed boundary layers, their source and available
242
+ vintage; analysis data is described as census-tract-based reporting areas, without inventing a
243
+ boundary year. The downloadable source metadata remains available in the full explorer.
244
+
245
+ Labels use measured screen-space collision boxes. Selected marker and neighborhood names
246
+ come first, then district labels, stations, parks, roads, and neighborhoods in descending area
247
+ order with stable ID tie-breaking. Station and marker symbols reserve space. Roads and station
248
+ names appear at 1.8× zoom; city view keeps park labels sparse. Labels outside the viewport or
249
+ without room are suppressed, including a selected label too wide to fit. Selection names remain
250
+ available in the native chooser and through events. The label range is separate from SVG user
251
+ units used by the static renderer.
252
+
253
+ ### Viewport and controlled selection
254
+
255
+ ```js
256
+ const saved = map.getViewport(); // copied [x, y, size] in the 800×800 projected map
257
+ map.zoomBy(2);
258
+ map.panBy(30, 0); // move view east by 30 screen pixels
259
+ map.setViewport(saved);
260
+ map.resetView();
261
+ map.fitGeometry({
262
+ type: 'MultiPoint',
263
+ coordinates: places.map(({ lng, lat }) => [lng, lat]),
264
+ }, { top: 24, right: 24, bottom: 80, left: 24 });
265
+ map.addEventListener('viewportchange', ({ detail }) => saveInYourState(detail.viewport));
266
+ ```
267
+
268
+ Call `fitGeometry` after mounting in a visible container. It accepts WGS84 GeoJSON geometry,
269
+ including `Point`, `MultiPoint`, and `GeometryCollection`, and rejects empty geometry or
270
+ impossible padding. Views are constrained to the city extent and 1–12× zoom. Consequently,
271
+ padding is best-effort near city edges or for extents larger than the map; a single point fits
272
+ at maximum zoom. This is an SF map, not a world map. Resizing keeps the projected view and
273
+ recomputes screen sizes; call `fitGeometry` again if a new container size needs different fitting.
274
+ Initial neighborhood fitting before mounting uses the explorer's proportional padding.
275
+
276
+ `selectNeighborhood(nameOrIdOrNull, { fit: false })` and `selectMarker(idOrNull, { fit: false })`
277
+ allow external state to drive selection without moving the viewport. They return false for
278
+ unknown identities. `getSelection()` and `getSelectedMarker()` read current selection.
279
+ `setSource(source)` clears neighborhood selection and resets the view, emitting a clear event
280
+ when needed. `setMode(mode)` resets the viewport. Repeating the same viewport or selection
281
+ emits no change event, avoiding state feedback loops. All events bubble. Call `destroy()`
282
+ before removing the element to release listeners, observers, frames, and download URLs.
283
+
284
+ ### Dense markers
285
+
286
+ ```js
287
+ const map = createInteractiveSFMap({ markers: places });
288
+ document.querySelector('#map').append(map);
289
+ map.addEventListener('markerchange', ({ detail }) => {
290
+ // { id, marker }, both null when cleared. Render your own content panel here.
291
+ renderSelection(detail.marker);
292
+ });
293
+ map.setMarkers(updatedPlaces);
294
+ map.selectMarker('place-id');
295
+ ```
296
+
297
+ Every supplied marker remains in the native chooser, even when positions coincide or markers
298
+ are outside the current view. Pointer and keyboard activation select and fit a marker; markers
299
+ also have individual keyboard stops. This is the accessible-choice alternative to clustering:
300
+ no counts are estimated and no supplied markers are silently dropped. The chooser count is
301
+ the supplied collection size. Replacing markers retains the selected ID when present, otherwise
302
+ uses an explicitly selected marker or clears selection. Large hit targets can overlap; use the
303
+ chooser to reach obscured markers. Automated clustering is not included in this release.
304
+
305
+ ### Gesture policy and keyboard access
306
+
307
+ - **Default touch:** one finger scrolls the page; pinch zooms the browser. Map buttons and
308
+ native choosers work without engaging map gestures.
309
+ - **Touch navigation:** explicitly enable the visible button (or `setTouchNavigation(true)`).
310
+ One finger pans the map; two fingers pan and pinch around their midpoint. The button becomes
311
+ **Done: page scrolling**. Use it or Escape to return to page gestures. The page remains
312
+ scrollable outside the canvas, and Tab can leave it. Changing mode during an active gesture
313
+ takes effect after fingers are lifted.
314
+ - **Mouse:** drag pans; Ctrl/⌘ + wheel zooms. Ordinary wheel scrolling remains page scrolling.
315
+ - **Keyboard:** focus the map, then arrows pan, +/− zoom, Home resets, and Escape exits touch
316
+ navigation. Tab reaches controls, the neighborhood chooser, one neighborhood path, and markers.
317
+ On a neighborhood path, `[` / `]` moves through source features and Enter/Space selects.
318
+ The native chooser is also available for areas outside the current view.
319
+ - Pointer cancellation, loss of capture, window blur, and resizing cancel active gestures.
320
+ There is no animated camera or inertia. Button transitions are disabled with reduced motion.
321
+
322
+ ### Examples and verification
323
+
324
+ Run `pnpm demo` and serve the repository root. `examples/generated/index.html` covers static
325
+ maps, `explorer.html` covers the full explorer, and `interactive.html` covers independently
326
+ controlled neighborhood selection, 36 overlapping sample markers, and fit/save/restore hooks.
327
+ Storybook **Maps / Reusable interactive map** includes both themes, selectable neighborhoods,
328
+ independent layers, dense markers, and a 390px example.
329
+
330
+ With that server running, the browser regression checks can be run through the installed CLI:
331
+
332
+ ```sh
333
+ agent-browser skills get core --full
334
+ agent-browser --session sf-map-check open http://127.0.0.1:8765/examples/generated/interactive.html
335
+ agent-browser --session sf-map-check eval "import('/scripts/check-interactive-browser.mjs').then(m => m.checkInteractiveBrowser())"
336
+ agent-browser --session sf-map-check close
337
+ ```
338
+
339
+ These checks cover actual browser layout, both themes, narrow/wide containers, label collisions,
340
+ zoom extremes, keyboard selection, cancellation logic, viewport state, marker reachability, and
341
+ teardown. Physical iOS Safari and Android Chrome verification remains required before a release:
342
+ check page scroll and browser pinch in default mode; map pan and pinch in engaged mode; lift one
343
+ finger; interrupt/cancel; rotate; use Done; verify scrolling resumes. Desktop automation and
344
+ synthetic pointer tests do not establish physical-device compatibility.
package/SOURCES.md CHANGED
@@ -75,3 +75,7 @@ No survey of resident consensus is claimed. Composite areas are not converted to
75
75
  On September 25, 2026, pairwise polygon intersection checks found 64 overlapping pairs in the original 92-area realtor dataset. These were boundary slivers totaling approximately 1.05 square meters (local planar estimate). `pnpm data:normalize-realtor` removes shared interior area by assigning it to the lexicographically first stable neighborhood ID and subtracting it from the other feature. This is a deterministic geometric tie-break for source slivers, not a new claim about legal boundaries.
76
76
 
77
77
  The cleanup retains all 92 identities, names, and source codes. It uses no rounding or buffers, recalculates affected bounding boxes, and verifies that the union of all neighborhood areas is unchanged. Shared edges and vertices remain valid. `data/neighborhoods-realtor.json` records the transformation in `topology`; its polygons are normalized derivatives of the cited source. Regression tests require empty pairwise polygon intersections, non-overlapping component polygons, the original union digest, and an idempotent cleanup. Polygon clipping is a development dependency only; rendering remains dependency-free.
78
+
79
+ ## Key road landmarks
80
+
81
+ Downloaded September 26, 2026 (UTC; September 25 in San Francisco) from [DataSF Streets – Active and Retired](https://data.sf.gov/resource/3psu-pn9h.geojson), filtering `active = true` and exact source street names. `data/key-roads.json` groups 724 source segments into nine named corridors, preserving every source coordinate and CNN segment ID. Geary St and Geary Blvd are grouped under the Geary Blvd display label. These are geographic orientation features, not a complete network or vehicle-access guidance. Label anchors select existing source vertices near editorial targets. Regenerate with `node scripts/import-key-roads.mjs` then `pnpm data:catalog`. The full query and retrieval date are embedded in the JSON. DataSF terms apply.
@@ -16,6 +16,7 @@ The renderer, `neighborhoods` convenience export, and `getNeighborhood` default
16
16
  | `neighborhoods-realtor.json` | All 92 SFAR areas in the August 2010 dataset |
17
17
  | `coast.json` | The renderer's common display coastline |
18
18
  | `highways.json` | The original map's highway geometry |
19
+ | `key-roads.json` | Nine selected road corridors with source segment IDs and label anchors |
19
20
  | `landmarks.json` | Six selected park/landmark property areas |
20
21
  | `bart-stations.json` | Eight San Francisco station points |
21
22
  | `catalog.json` | Dataset index and searchable neighborhood metadata without geometry |
@@ -0,0 +1,134 @@
1
+ {
2
+ "type": "FeatureCollection",
3
+ "schemaVersion": 1,
4
+ "id": "bart-stations",
5
+ "title": "San Francisco BART stations",
6
+ "coordinateSystem": "WGS84 longitude, latitude (EPSG:4326)",
7
+ "definition": {
8
+ "kind": "station-centroids",
9
+ "description": "Eight station centroids within San Francisco. Entrances, tracks, and other cities are excluded."
10
+ },
11
+ "sources": [
12
+ {
13
+ "id": "bart-stations",
14
+ "title": "BART official geospatial data",
15
+ "url": "https://www.bart.gov/sites/default/files/2025-12/BART-Stations-tracks-entrances-121025.kmz_.zip",
16
+ "retrievedAt": "2026-09-25",
17
+ "licenseUrl": "https://www.bart.gov/schedules/developers/geo"
18
+ }
19
+ ],
20
+ "features": [
21
+ {
22
+ "type": "Feature",
23
+ "id": "embarcadero",
24
+ "bbox": [
25
+ -122.3969009943399,
26
+ 37.79285391372556,
27
+ -122.3969009943399,
28
+ 37.79285391372556
29
+ ],
30
+ "properties": {
31
+ "name": "Embarcadero"
32
+ },
33
+ "geometry": { "type": "Point", "coordinates": [-122.3969009943399, 37.79285391372556] }
34
+ },
35
+ {
36
+ "type": "Feature",
37
+ "id": "montgomery-st",
38
+ "bbox": [
39
+ -122.4011747105981,
40
+ 37.78944952039284,
41
+ -122.4011747105981,
42
+ 37.78944952039284
43
+ ],
44
+ "properties": {
45
+ "name": "Montgomery St"
46
+ },
47
+ "geometry": { "type": "Point", "coordinates": [-122.4011747105981, 37.78944952039284] }
48
+ },
49
+ {
50
+ "type": "Feature",
51
+ "id": "powell-st",
52
+ "bbox": [
53
+ -122.4070176588271,
54
+ 37.78487089662774,
55
+ -122.4070176588271,
56
+ 37.78487089662774
57
+ ],
58
+ "properties": {
59
+ "name": "Powell St"
60
+ },
61
+ "geometry": { "type": "Point", "coordinates": [-122.4070176588271, 37.78487089662774] }
62
+ },
63
+ {
64
+ "type": "Feature",
65
+ "id": "civic-center-un-plaza",
66
+ "bbox": [
67
+ -122.4139364152156,
68
+ 37.77939386076284,
69
+ -122.4139364152156,
70
+ 37.77939386076284
71
+ ],
72
+ "properties": {
73
+ "name": "Civic Center/UN Plaza"
74
+ },
75
+ "geometry": { "type": "Point", "coordinates": [-122.4139364152156, 37.77939386076284] }
76
+ },
77
+ {
78
+ "type": "Feature",
79
+ "id": "16th-st-mission",
80
+ "bbox": [
81
+ -122.4197081435103,
82
+ 37.76505423932072,
83
+ -122.4197081435103,
84
+ 37.76505423932072
85
+ ],
86
+ "properties": {
87
+ "name": "16th St/Mission"
88
+ },
89
+ "geometry": { "type": "Point", "coordinates": [-122.4197081435103, 37.76505423932072] }
90
+ },
91
+ {
92
+ "type": "Feature",
93
+ "id": "24th-st-mission",
94
+ "bbox": [
95
+ -122.4184678760468,
96
+ 37.75223145165559,
97
+ -122.4184678760468,
98
+ 37.75223145165559
99
+ ],
100
+ "properties": {
101
+ "name": "24th St/Mission"
102
+ },
103
+ "geometry": { "type": "Point", "coordinates": [-122.4184678760468, 37.75223145165559] }
104
+ },
105
+ {
106
+ "type": "Feature",
107
+ "id": "glen-park",
108
+ "bbox": [
109
+ -122.4337900883829,
110
+ 37.73311239451952,
111
+ -122.4337900883829,
112
+ 37.73311239451952
113
+ ],
114
+ "properties": {
115
+ "name": "Glen Park"
116
+ },
117
+ "geometry": { "type": "Point", "coordinates": [-122.4337900883829, 37.73311239451952] }
118
+ },
119
+ {
120
+ "type": "Feature",
121
+ "id": "balboa-park",
122
+ "bbox": [
123
+ -122.4475873574919,
124
+ 37.72135058802738,
125
+ -122.4475873574919,
126
+ 37.72135058802738
127
+ ],
128
+ "properties": {
129
+ "name": "Balboa Park"
130
+ },
131
+ "geometry": { "type": "Point", "coordinates": [-122.4475873574919, 37.72135058802738] }
132
+ }
133
+ ]
134
+ }