@kahwee/sf-map-svg 1.3.8 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.4.0 — 2026-09-26
8
+
9
+ - Split geographic convenience exports into independent data modules, so metadata search and SFAR-only lookup avoid unrelated JSON.
10
+ - Separate the guide's detailed loader from overview geography; keep the existing `/guide` exports and add explicit `/guide/detailed`, `/guide/data`, and `/guide/map` paths.
11
+ - Make the optional transit animation include only its 2022 map geography, preserving the rendered route while cutting its consumer bundle size.
12
+ - Publish a before-and-after tree-shaking report for representative consumer imports.
13
+
7
14
  ## 1.3.8 — 2026-09-26
8
15
 
9
16
  - Give the district map a clear, dedicated viewing area on mobile by shrinking the election and measure controls and moving the map legend below the map.
package/README.md CHANGED
@@ -26,6 +26,8 @@ Use Node 22.12+ for server-side rendering. Browser components need a DOM and a b
26
26
  | Lightweight guide map | `@kahwee/sf-map-svg/guide` | Curated overview geography, with detailed data loaded explicitly |
27
27
  | Animated transit demo | `@kahwee/sf-map-svg/transit` | Optional, schematic BART journey with playback controls |
28
28
  | Geographic data | `@kahwee/sf-map-svg/data` | Source-aware lookup and canonical GeoJSON |
29
+ | Metadata search | `@kahwee/sf-map-svg/data/catalog` | Search names without polygon geometry |
30
+ | SFAR lookup | `@kahwee/sf-map-svg/data/realtor` | Default neighborhoods without alternative sources |
29
31
 
30
32
  ## Render a static map
31
33
 
@@ -53,6 +55,14 @@ road, park, and station arrays. Use the canonical JSON subpaths documented in
53
55
  corresponding `SFMapData` records. The root entry remains convenient and includes the
54
56
  built-in datasets for backward compatibility.
55
57
 
58
+ In tree-shaking bundlers, importing a single symbol from `/data` retains only the
59
+ modules that symbol uses.
60
+ `searchNeighborhoods` needs catalog metadata but no polygon geometry. The synchronous
61
+ source-switching `getNeighborhood` still needs all three neighborhood collections;
62
+ use `/data/realtor` when only the default SFAR definitions are needed. Optional coast,
63
+ district, road, park, and station collections also have independent `/data/*` entries.
64
+ See [the measured bundle report](docs/module-bundle-report.md).
65
+
56
66
  The same split is available for browser controls with
57
67
  `@kahwee/sf-map-svg/interactive-data`. Pass an `InteractiveSFMapData` object containing
58
68
  `map` (the `SFMapData` used by the static renderer) and only the neighborhood collections
@@ -69,7 +79,8 @@ have independent `layers.keyRoads` and `layers.roadLabels` controls. Lombard geo
69
79
  label appear after 1.8× zoom; major street labels can appear at city scale in a smaller type size.
70
80
 
71
81
  ```ts
72
- import { createGuideMap, loadGuideDetailedData } from '@kahwee/sf-map-svg/guide';
82
+ import { createGuideMap } from '@kahwee/sf-map-svg/guide';
83
+ import { loadGuideDetailedData } from '@kahwee/sf-map-svg/guide/detailed';
73
84
  import { createInteractiveSFMapWithData } from '@kahwee/sf-map-svg/interactive-data';
74
85
 
75
86
  const map = createGuideMap({ layers: { roadLabels: false } });
@@ -85,6 +96,8 @@ const detailedMap = createInteractiveSFMapWithData(detailedData, {
85
96
 
86
97
  Run `pnpm data:guide` to rebuild the subpixel overview geometries from canonical files.
87
98
  `docs/guide-bundle-report.md` records compressed bundle sizes and verifies the included data.
99
+ The detailed loader is also re-exported from `/guide` for compatibility; importing only
100
+ that function no longer puts overview geography in the initial bundle.
88
101
 
89
102
  ```js
90
103
  import coast from '@kahwee/sf-map-svg/data/coast.json' with { type: 'json' };
@@ -328,13 +341,12 @@ chooser to reach obscured markers. Automated clustering is not included in this
328
341
  All map geometry is available through stable JSON package exports. There are three district files (2002, 2012, 2022), the full 117 SF Find neighborhoods, 41 analysis neighborhoods, 92 realtor-defined areas, and separate coastline, highway, landmark, and BART files. Neighborhood records include a canonical display name, exact source name, stable ID, aliases where documented, source definition, and full polygon geometry.
329
342
 
330
343
  ```js
331
- import neighborhoods from '@kahwee/sf-map-svg/data/neighborhoods-realtor.json' with { type: 'json' };
332
- import districts2022 from '@kahwee/sf-map-svg/data/districts-2022.json' with { type: 'json' };
333
- import { getNeighborhood, searchNeighborhoods } from '@kahwee/sf-map-svg/data';
344
+ import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
345
+ import { searchNeighborhoods } from '@kahwee/sf-map-svg/data/catalog';
334
346
 
335
- const mission = getNeighborhood('Inner Mission');
336
- const outerMission = getNeighborhood('Outer Mission');
337
- const nopa = getNeighborhood('NoPa', { source: 'realtor' });
347
+ const mission = getRealtorNeighborhood('Inner Mission');
348
+ const outerMission = getRealtorNeighborhood('Outer Mission');
349
+ const nopa = getRealtorNeighborhood('NoPa');
338
350
  const matchingDefinitions = searchNeighborhoods('mission');
339
351
  ```
340
352
 
@@ -33,7 +33,28 @@ const mission = neighborhoods.features.find((f) => f.id === 'inner-mission');
33
33
  console.log(mission.properties.canonicalName, mission.geometry);
34
34
  ```
35
35
 
36
- Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly. These are package entry points, not a hosted API. The public repository and GitHub Pages explorer also provide access to the source data. Consumers who need only one dataset should import its JSON subpath rather than the convenience module, which loads all collections.
36
+ Node 22.12+ supports this syntax. Bundlers may also support JSON imports without the import attribute. Files are included in the package archive; non-JavaScript consumers can parse the same JSON files directly. These are package entry points, not a hosted API. The public repository and GitHub Pages explorer also provide access to the source data. Consumers who need only one dataset can import its JSON subpath or a typed, frozen leaf module.
37
+
38
+ ## Import only the data you need
39
+
40
+ ```js
41
+ import { searchNeighborhoods } from '@kahwee/sf-map-svg/data/catalog';
42
+ import { getRealtorNeighborhood } from '@kahwee/sf-map-svg/data/realtor';
43
+ import { coast } from '@kahwee/sf-map-svg/data/coast';
44
+
45
+ const candidates = searchNeighborhoods('mission'); // metadata, no polygons
46
+ const mission = getRealtorNeighborhood('Inner Mission'); // SFAR only
47
+ const coastline = coast.features[0].geometry;
48
+ ```
49
+
50
+ The `/data` entry remains compatible and re-exports these helpers and collections.
51
+ Its `getNeighborhood(name, { source })` imports all three neighborhood definitions
52
+ because source switching is synchronous. Importing only `searchNeighborhoods` from
53
+ `/data` now includes catalog metadata only in tree-shaking bundles. Other independent modules are
54
+ `/data/sf-find`, `/data/analysis`, `/data/districts`, `/data/highways`,
55
+ `/data/landmarks`, `/data/roads`, and `/data/stations`. Districts still groups all
56
+ three supported years; import `districts-2022.json` directly when only one year is
57
+ needed. See [the consumer bundle report](../docs/module-bundle-report.md).
37
58
 
38
59
  ## Names and definitions
39
60
 
@@ -69,7 +90,7 @@ const allAnalysisPolygons = neighborhoodCollections.analysis;
69
90
  const historicalDistricts = districtMaps[2012];
70
91
  ```
71
92
 
72
- Lookup is exact after case/punctuation normalization, returns `undefined` for an unknown name, and throws for an unknown source. Search matches substrings and preserves every source-specific result. The convenience module's shared data is deeply frozen; use `structuredClone(feature)` if you need an editable copy. Do not mutate imported datasets used by the renderer.
93
+ Lookup is exact after case/punctuation normalization, returns `undefined` for an unknown name, and throws for an unknown source. Search matches substrings and preserves every source-specific result. The convenience and leaf modules' shared data is deeply frozen; use `structuredClone(feature)` if you need an editable copy. Do not mutate imported datasets used by the renderer.
73
94
 
74
95
  ## Draw a neighborhood with the existing projection
75
96
 
@@ -0,0 +1,2 @@
1
+ import type { FeatureCollection, NeighborhoodProperties } from './types.js';
2
+ export declare const analysisNeighborhoods: FeatureCollection<NeighborhoodProperties>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import analysisData from './neighborhoods-analysis.json' with { type: 'json' };
3
+ export const analysisNeighborhoods = deepFreeze(analysisData);
@@ -0,0 +1,6 @@
1
+ import type { Catalog, NeighborhoodSource } from './types.js';
2
+ export declare const catalog: Catalog;
3
+ /** Search name metadata across sources without importing polygon geometry. */
4
+ export declare function searchNeighborhoods(query?: string, { source }?: {
5
+ source?: NeighborhoodSource;
6
+ }): import("./types.js").NeighborhoodEntry[];
@@ -0,0 +1,14 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import catalogData from './catalog.json' with { type: 'json' };
3
+ import { normalizeNeighborhoodName } from './lookup-utils.js';
4
+ export const catalog = deepFreeze(catalogData);
5
+ /** Search name metadata across sources without importing polygon geometry. */
6
+ export function searchNeighborhoods(query = '', { source } = {}) {
7
+ if (source !== undefined && !['sf-find', 'analysis', 'realtor'].includes(source))
8
+ throw new RangeError(`Unknown neighborhood source: ${source}`);
9
+ if (typeof query !== 'string')
10
+ throw new TypeError('Neighborhood query must be a string.');
11
+ const term = normalizeNeighborhoodName(query);
12
+ return catalog.neighborhoods.filter((entry) => (source === undefined || entry.source === source) &&
13
+ [entry.id, entry.canonicalName, entry.sourceName, ...entry.aliases].some((value) => normalizeNeighborhoodName(value).includes(term)));
14
+ }
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection } from './types.js';
2
+ export declare const coast: FeatureCollection<{
3
+ readonly name: string;
4
+ }>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import coastData from './coast.json' with { type: 'json' };
3
+ export const coast = deepFreeze(coastData);
@@ -0,0 +1,2 @@
1
+ import type { DistrictProperties, FeatureCollection } from './types.js';
2
+ export declare const districtMaps: Readonly<Record<2002 | 2012 | 2022, FeatureCollection<DistrictProperties>>>;
@@ -0,0 +1,9 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import districts2002 from './districts-2002.json' with { type: 'json' };
3
+ import districts2012 from './districts-2012.json' with { type: 'json' };
4
+ import districts2022 from './districts-2022.json' with { type: 'json' };
5
+ export const districtMaps = deepFreeze({
6
+ 2002: districts2002,
7
+ 2012: districts2012,
8
+ 2022: districts2022,
9
+ });
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection } from './types.js';
2
+ export declare const highways: FeatureCollection<{
3
+ readonly route: string;
4
+ }>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import highwayData from './highways.json' with { type: 'json' };
3
+ export const highways = deepFreeze(highwayData);
@@ -1,26 +1,10 @@
1
+ export { catalog, searchNeighborhoods } from './catalog.js';
2
+ export { coast } from './coast.js';
3
+ export { districtMaps } from './districts.js';
4
+ export { highways } from './highways.js';
5
+ export { landmarks } from './landmarks.js';
6
+ export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
7
+ export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
8
+ export { keyRoads } from './roads.js';
9
+ export { bartStations } from './stations.js';
1
10
  export type * from './types.js';
2
- import type { Catalog, DistrictProperties, Feature, FeatureCollection, KeyRoadProperties, LandmarkProperties, NeighborhoodProperties, NeighborhoodSource } from './types.js';
3
- export declare const coast: FeatureCollection<{
4
- readonly name: string;
5
- }>;
6
- export declare const districtMaps: Readonly<Record<2002 | 2012 | 2022, FeatureCollection<DistrictProperties>>>;
7
- export declare const neighborhoods: FeatureCollection<NeighborhoodProperties>;
8
- export declare const neighborhoodCollections: Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>;
9
- export declare const neighborhoodSources: readonly NeighborhoodSource[];
10
- export declare const highways: FeatureCollection<{
11
- readonly route: string;
12
- }>;
13
- export declare const landmarks: FeatureCollection<LandmarkProperties>;
14
- export declare const bartStations: FeatureCollection<{
15
- readonly name: string;
16
- }>;
17
- export declare const catalog: Catalog;
18
- /** Exact ID, canonical name, source name, or alias lookup within one definition set. */
19
- export declare function getNeighborhood(name: string, { source }?: {
20
- source?: NeighborhoodSource;
21
- }): Feature<NeighborhoodProperties> | undefined;
22
- /** Search names across sources; results keep their source identity and omit geometry. */
23
- export declare function searchNeighborhoods(query?: string, { source }?: {
24
- source?: NeighborhoodSource;
25
- }): import("./types.js").NeighborhoodEntry[];
26
- export declare const keyRoads: FeatureCollection<KeyRoadProperties>;
@@ -1,67 +1,9 @@
1
- import { deepFreeze } from '../src/immutable.js';
2
- import stationData from './bart-stations.json' with { type: 'json' };
3
- import catalogData from './catalog.json' with { type: 'json' };
4
- import coastData from './coast.json' with { type: 'json' };
5
- import districts2002 from './districts-2002.json' with { type: 'json' };
6
- import districts2012 from './districts-2012.json' with { type: 'json' };
7
- import districts2022 from './districts-2022.json' with { type: 'json' };
8
- import highwayData from './highways.json' with { type: 'json' };
9
- import roadData from './key-roads.json' with { type: 'json' };
10
- import landmarkData from './landmarks.json' with { type: 'json' };
11
- import sfFind from './neighborhoods.json' with { type: 'json' };
12
- import analysis from './neighborhoods-analysis.json' with { type: 'json' };
13
- import realtor from './neighborhoods-realtor.json' with { type: 'json' };
14
- export const coast = deepFreeze(coastData);
15
- export const districtMaps = deepFreeze({
16
- 2002: districts2002,
17
- 2012: districts2012,
18
- 2022: districts2022,
19
- });
20
- export const neighborhoods = deepFreeze(realtor);
21
- export const neighborhoodCollections = deepFreeze({
22
- 'sf-find': sfFind,
23
- analysis,
24
- realtor,
25
- });
26
- export const neighborhoodSources = Object.freeze(Object.keys(neighborhoodCollections));
27
- export const highways = deepFreeze(highwayData);
28
- export const landmarks = deepFreeze(landmarkData);
29
- export const bartStations = deepFreeze(stationData);
30
- export const catalog = deepFreeze(catalogData);
31
- const normalize = (value) => value
32
- .normalize('NFKD')
33
- .toLowerCase()
34
- .replace(/[\u0300-\u036f'’]/g, '')
35
- .replace(/[^a-z0-9]+/g, ' ')
36
- .trim();
37
- function checkSource(source) {
38
- if (!Object.hasOwn(neighborhoodCollections, source))
39
- throw new RangeError(`Unknown neighborhood source: ${source}`);
40
- }
41
- const names = (feature) => [
42
- feature.id,
43
- feature.properties.canonicalName,
44
- feature.properties.sourceName,
45
- ...feature.properties.aliases,
46
- ];
47
- /** Exact ID, canonical name, source name, or alias lookup within one definition set. */
48
- export function getNeighborhood(name, { source = 'realtor' } = {}) {
49
- checkSource(source);
50
- if (typeof name !== 'string')
51
- throw new TypeError('Neighborhood name must be a string.');
52
- const query = normalize(name);
53
- if (!query)
54
- return undefined;
55
- return neighborhoodCollections[source].features.find((feature) => names(feature).some((value) => normalize(value) === query));
56
- }
57
- /** Search names across sources; results keep their source identity and omit geometry. */
58
- export function searchNeighborhoods(query = '', { source } = {}) {
59
- if (source !== undefined)
60
- checkSource(source);
61
- if (typeof query !== 'string')
62
- throw new TypeError('Neighborhood query must be a string.');
63
- const term = normalize(query);
64
- return catalog.neighborhoods.filter((entry) => (source === undefined || entry.source === source) &&
65
- [entry.id, entry.canonicalName, entry.sourceName, ...entry.aliases].some((value) => normalize(value).includes(term)));
66
- }
67
- export const keyRoads = deepFreeze(roadData);
1
+ export { catalog, searchNeighborhoods } from './catalog.js';
2
+ export { coast } from './coast.js';
3
+ export { districtMaps } from './districts.js';
4
+ export { highways } from './highways.js';
5
+ export { landmarks } from './landmarks.js';
6
+ export { getNeighborhood, neighborhoodCollections, neighborhoodSources } from './lookup.js';
7
+ export { getRealtorNeighborhood, neighborhoods } from './realtor.js';
8
+ export { keyRoads } from './roads.js';
9
+ export { bartStations } from './stations.js';
@@ -0,0 +1,2 @@
1
+ import type { FeatureCollection, LandmarkProperties } from './types.js';
2
+ export declare const landmarks: FeatureCollection<LandmarkProperties>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import landmarkData from './landmarks.json' with { type: 'json' };
3
+ export const landmarks = deepFreeze(landmarkData);
@@ -0,0 +1,3 @@
1
+ import type { Feature, FeatureCollection, NeighborhoodProperties } from './types.js';
2
+ export declare const normalizeNeighborhoodName: (value: string) => string;
3
+ export declare function findNeighborhood(name: string, collection: FeatureCollection<NeighborhoodProperties>): Feature<NeighborhoodProperties> | undefined;
@@ -0,0 +1,14 @@
1
+ export const normalizeNeighborhoodName = (value) => value
2
+ .normalize('NFKD')
3
+ .toLowerCase()
4
+ .replace(/[\u0300-\u036f'’]/g, '')
5
+ .replace(/[^a-z0-9]+/g, ' ')
6
+ .trim();
7
+ export function findNeighborhood(name, collection) {
8
+ if (typeof name !== 'string')
9
+ throw new TypeError('Neighborhood name must be a string.');
10
+ const query = normalizeNeighborhoodName(name);
11
+ if (!query)
12
+ return undefined;
13
+ return collection.features.find(({ id, properties }) => [id, properties.canonicalName, properties.sourceName, ...properties.aliases].some((value) => normalizeNeighborhoodName(value) === query));
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { FeatureCollection, NeighborhoodProperties, NeighborhoodSource } from './types.js';
2
+ export declare const neighborhoodCollections: Readonly<Record<NeighborhoodSource, FeatureCollection<NeighborhoodProperties>>>;
3
+ export declare const neighborhoodSources: readonly NeighborhoodSource[];
4
+ /** Exact ID, canonical name, source name, or alias lookup within one definition set. */
5
+ export declare function getNeighborhood(name: string, { source }?: {
6
+ source?: NeighborhoodSource;
7
+ }): import("./types.js").Feature<NeighborhoodProperties> | undefined;
@@ -0,0 +1,16 @@
1
+ import { analysisNeighborhoods } from './analysis.js';
2
+ import { findNeighborhood } from './lookup-utils.js';
3
+ import { neighborhoods } from './realtor.js';
4
+ import { sfFindNeighborhoods } from './sf-find.js';
5
+ export const neighborhoodCollections = Object.freeze({
6
+ 'sf-find': sfFindNeighborhoods,
7
+ analysis: analysisNeighborhoods,
8
+ realtor: neighborhoods,
9
+ });
10
+ export const neighborhoodSources = Object.freeze(Object.keys(neighborhoodCollections));
11
+ /** Exact ID, canonical name, source name, or alias lookup within one definition set. */
12
+ export function getNeighborhood(name, { source = 'realtor' } = {}) {
13
+ if (!Object.hasOwn(neighborhoodCollections, source))
14
+ throw new RangeError(`Unknown neighborhood source: ${source}`);
15
+ return findNeighborhood(name, neighborhoodCollections[source]);
16
+ }
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection, NeighborhoodProperties } from './types.js';
2
+ export declare const neighborhoods: FeatureCollection<NeighborhoodProperties>;
3
+ /** Look up one SFAR area without importing the other neighborhood definitions. */
4
+ export declare function getRealtorNeighborhood(name: string): import("./types.js").Feature<NeighborhoodProperties> | undefined;
@@ -0,0 +1,8 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import { findNeighborhood } from './lookup-utils.js';
3
+ import realtorData from './neighborhoods-realtor.json' with { type: 'json' };
4
+ export const neighborhoods = deepFreeze(realtorData);
5
+ /** Look up one SFAR area without importing the other neighborhood definitions. */
6
+ export function getRealtorNeighborhood(name) {
7
+ return findNeighborhood(name, neighborhoods);
8
+ }
@@ -0,0 +1,2 @@
1
+ import type { FeatureCollection, KeyRoadProperties } from './types.js';
2
+ export declare const keyRoads: FeatureCollection<KeyRoadProperties>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import roadData from './key-roads.json' with { type: 'json' };
3
+ export const keyRoads = deepFreeze(roadData);
@@ -0,0 +1,2 @@
1
+ import type { FeatureCollection, NeighborhoodProperties } from './types.js';
2
+ export declare const sfFindNeighborhoods: FeatureCollection<NeighborhoodProperties>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import sfFindData from './neighborhoods.json' with { type: 'json' };
3
+ export const sfFindNeighborhoods = deepFreeze(sfFindData);
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection } from './types.js';
2
+ export declare const bartStations: FeatureCollection<{
3
+ readonly name: string;
4
+ }>;
@@ -0,0 +1,3 @@
1
+ import { deepFreeze } from '../src/immutable.js';
2
+ import stationData from './bart-stations.json' with { type: 'json' };
3
+ export const bartStations = deepFreeze(stationData);
@@ -0,0 +1,3 @@
1
+ import type { InteractiveSFMapData } from './explorer-data.js';
2
+ /** Compact, source-aware geography with no district or alternate-neighborhood files. */
3
+ export declare const guideMapData: InteractiveSFMapData;
@@ -0,0 +1,41 @@
1
+ import bart from '../data/guide/bart-stations.json' with { type: 'json' };
2
+ import coast from '../data/guide/coast.json' with { type: 'json' };
3
+ import highways from '../data/guide/highways.json' with { type: 'json' };
4
+ import roads from '../data/guide/key-roads.json' with { type: 'json' };
5
+ import landmarks from '../data/guide/landmarks.json' with { type: 'json' };
6
+ import neighborhoods from '../data/guide/neighborhoods-realtor.json' with { type: 'json' };
7
+ const neighborhoodData = neighborhoods;
8
+ const highwayData = highways;
9
+ const parkData = landmarks;
10
+ const roadData = roads;
11
+ const stationData = bart;
12
+ /** Compact, source-aware geography with no district or alternate-neighborhood files. */
13
+ export const guideMapData = {
14
+ map: {
15
+ coast: coast.features[0].geometry,
16
+ neighborhoods: neighborhoodData.features.map((feature) => ({
17
+ name: feature.properties.canonicalName,
18
+ geometry: feature.geometry,
19
+ })),
20
+ highways: highwayData.features.map((feature) => ({
21
+ route: String(feature.properties.route),
22
+ geometry: feature.geometry,
23
+ })),
24
+ landmarks: parkData.features.map(({ id, properties, geometry }) => ({
25
+ id,
26
+ ...properties,
27
+ geometry,
28
+ })),
29
+ keyRoads: roadData.features.map(({ id, properties, geometry }) => ({
30
+ id,
31
+ ...properties,
32
+ sourceNames: properties.sourceNames ?? [],
33
+ segmentIds: properties.segmentIds ?? [],
34
+ geometry,
35
+ })),
36
+ bartStations: stationData.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
37
+ ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
38
+ : []),
39
+ },
40
+ neighborhoods: { realtor: neighborhoodData },
41
+ };
@@ -0,0 +1,3 @@
1
+ import type { InteractiveSFMapData } from './explorer-data.js';
2
+ /** Load detailed selected geography only after the caller requests it. */
3
+ export declare function loadGuideDetailedData(): Promise<InteractiveSFMapData>;
@@ -0,0 +1,45 @@
1
+ /** Load detailed selected geography only after the caller requests it. */
2
+ export async function loadGuideDetailedData() {
3
+ const [coastData, neighborhoodData, parkData, highwayData, roadData, bartData] = await Promise.all([
4
+ import('../data/coast.json', { with: { type: 'json' } }),
5
+ import('../data/neighborhoods-realtor.json', { with: { type: 'json' } }),
6
+ import('../data/landmarks.json', { with: { type: 'json' } }),
7
+ import('../data/guide/highways-detailed.json', { with: { type: 'json' } }),
8
+ import('../data/key-roads.json', { with: { type: 'json' } }),
9
+ import('../data/bart-stations.json', { with: { type: 'json' } }),
10
+ ]);
11
+ const detailedNeighborhoods = neighborhoodData.default;
12
+ const detailedRoads = roadData.default;
13
+ const detailedHighways = highwayData.default;
14
+ const detailedParks = parkData.default;
15
+ const detailedStations = bartData.default;
16
+ return {
17
+ map: {
18
+ coast: coastData.default.features[0].geometry,
19
+ neighborhoods: detailedNeighborhoods.features.map((feature) => ({
20
+ name: feature.properties.canonicalName,
21
+ geometry: feature.geometry,
22
+ })),
23
+ highways: detailedHighways.features
24
+ .filter((feature) => ['1', '101', '280'].includes(String(feature.properties.route)))
25
+ .map((feature) => ({
26
+ route: String(feature.properties.route),
27
+ geometry: feature.geometry,
28
+ })),
29
+ landmarks: detailedParks.features.map(({ id, properties, geometry }) => ({
30
+ id,
31
+ ...properties,
32
+ geometry,
33
+ })),
34
+ keyRoads: detailedRoads.features.map(({ id, properties, geometry }) => ({
35
+ id,
36
+ ...properties,
37
+ geometry,
38
+ })),
39
+ bartStations: detailedStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
40
+ ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
41
+ : []),
42
+ },
43
+ neighborhoods: { realtor: detailedNeighborhoods },
44
+ };
45
+ }
@@ -0,0 +1,3 @@
1
+ import type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
2
+ /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
3
+ export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
@@ -0,0 +1,22 @@
1
+ import { guideMapData } from './guide-data.js';
2
+ import { createInteractiveSFMapWithData } from './interactive-data.js';
3
+ /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
4
+ export function createGuideMap(options = {}) {
5
+ return createInteractiveSFMapWithData(guideMapData, {
6
+ mode: 'neighborhoods',
7
+ ...options,
8
+ layers: {
9
+ districtFills: false,
10
+ districtLines: false,
11
+ districtLabels: false,
12
+ neighborhoodLines: true,
13
+ neighborhoodLabels: true,
14
+ landmarks: true,
15
+ bartStations: true,
16
+ highways: true,
17
+ keyRoads: true,
18
+ roadLabels: true,
19
+ ...options.layers,
20
+ },
21
+ });
22
+ }
@@ -1,10 +1,5 @@
1
- import type { InteractiveSFMapData } from './explorer-data.js';
2
- import type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
3
- /** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
4
- export declare const guideMapData: InteractiveSFMapData;
5
- /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
6
- export declare function createGuideMap(options?: InteractiveSFMapOptions): InteractiveSFMapElement;
7
- /** Load the detailed versions of the guide's selected geography on explicit request. */
8
- export declare function loadGuideDetailedData(): Promise<InteractiveSFMapData>;
9
1
  export type { InteractiveSFMapData } from './explorer-data.js';
10
- export type { InteractiveSFMapElement, InteractiveSFMapOptions };
2
+ export { guideMapData } from './guide-data.js';
3
+ export { loadGuideDetailedData } from './guide-detailed.js';
4
+ export { createGuideMap } from './guide-map.js';
5
+ export type { InteractiveSFMapElement, InteractiveSFMapOptions } from './interactive-data.js';
package/dist/src/guide.js CHANGED
@@ -1,107 +1,3 @@
1
- import bart from '../data/guide/bart-stations.json' with { type: 'json' };
2
- import coast from '../data/guide/coast.json' with { type: 'json' };
3
- import highways from '../data/guide/highways.json' with { type: 'json' };
4
- import roads from '../data/guide/key-roads.json' with { type: 'json' };
5
- import landmarks from '../data/guide/landmarks.json' with { type: 'json' };
6
- import neighborhoods from '../data/guide/neighborhoods-realtor.json' with { type: 'json' };
7
- import { createInteractiveSFMapWithData } from './interactive-data.js';
8
- const neighborhoodData = neighborhoods;
9
- const highwayData = highways;
10
- const parkData = landmarks;
11
- const roadData = roads;
12
- const stationData = bart;
13
- /** Compact, source-aware San Francisco guide data. Imports no district or alternate-neighborhood files. */
14
- export const guideMapData = {
15
- map: {
16
- coast: coast.features[0].geometry,
17
- neighborhoods: neighborhoodData.features.map((feature) => ({
18
- name: feature.properties.canonicalName,
19
- geometry: feature.geometry,
20
- })),
21
- highways: highwayData.features.map((feature) => ({
22
- route: String(feature.properties.route),
23
- geometry: feature.geometry,
24
- })),
25
- landmarks: parkData.features.map(({ id, properties, geometry }) => ({
26
- id,
27
- ...properties,
28
- geometry,
29
- })),
30
- keyRoads: roadData.features.map(({ id, properties, geometry }) => ({
31
- id,
32
- ...properties,
33
- sourceNames: properties.sourceNames ?? [],
34
- segmentIds: properties.segmentIds ?? [],
35
- geometry,
36
- })),
37
- bartStations: stationData.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
38
- ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
39
- : []),
40
- },
41
- neighborhoods: { realtor: neighborhoodData },
42
- };
43
- /** Guide map preset: SFAR neighborhoods, major parks, BART, and curated roads. */
44
- export function createGuideMap(options = {}) {
45
- return createInteractiveSFMapWithData(guideMapData, {
46
- mode: 'neighborhoods',
47
- ...options,
48
- layers: {
49
- districtFills: false,
50
- districtLines: false,
51
- districtLabels: false,
52
- neighborhoodLines: true,
53
- neighborhoodLabels: true,
54
- landmarks: true,
55
- bartStations: true,
56
- highways: true,
57
- keyRoads: true,
58
- roadLabels: true,
59
- ...options.layers,
60
- },
61
- });
62
- }
63
- /** Load the detailed versions of the guide's selected geography on explicit request. */
64
- export async function loadGuideDetailedData() {
65
- const [coastData, neighborhoodData, parkData, highwayData, roadData, bartData] = await Promise.all([
66
- import('../data/coast.json', { with: { type: 'json' } }),
67
- import('../data/neighborhoods-realtor.json', { with: { type: 'json' } }),
68
- import('../data/landmarks.json', { with: { type: 'json' } }),
69
- import('../data/guide/highways-detailed.json', { with: { type: 'json' } }),
70
- import('../data/key-roads.json', { with: { type: 'json' } }),
71
- import('../data/bart-stations.json', { with: { type: 'json' } }),
72
- ]);
73
- const detailedNeighborhoods = neighborhoodData.default;
74
- const detailedRoads = roadData.default;
75
- const detailedHighways = highwayData.default;
76
- const detailedParks = parkData.default;
77
- const detailedStations = bartData.default;
78
- return {
79
- map: {
80
- coast: coastData.default.features[0].geometry,
81
- neighborhoods: detailedNeighborhoods.features.map((feature) => ({
82
- name: feature.properties.canonicalName,
83
- geometry: feature.geometry,
84
- })),
85
- highways: detailedHighways.features
86
- .filter((feature) => ['1', '101', '280'].includes(String(feature.properties.route)))
87
- .map((feature) => ({
88
- route: String(feature.properties.route),
89
- geometry: feature.geometry,
90
- })),
91
- landmarks: detailedParks.features.map(({ id, properties, geometry }) => ({
92
- id,
93
- ...properties,
94
- geometry,
95
- })),
96
- keyRoads: detailedRoads.features.map(({ id, properties, geometry }) => ({
97
- id,
98
- ...properties,
99
- geometry,
100
- })),
101
- bartStations: detailedStations.features.flatMap(({ id, properties, geometry }) => geometry.type === 'Point'
102
- ? [{ id, name: properties.name, coordinates: geometry.coordinates }]
103
- : []),
104
- },
105
- neighborhoods: { realtor: detailedNeighborhoods },
106
- };
107
- }
1
+ export { guideMapData } from './guide-data.js';
2
+ export { loadGuideDetailedData } from './guide-detailed.js';
3
+ export { createGuideMap } from './guide-map.js';
@@ -1,12 +1,34 @@
1
1
  import stations from '../data/bart-stations.json' with { type: 'json' };
2
- import { createSFMap } from './index.js';
2
+ import coast from '../data/coast.json' with { type: 'json' };
3
+ import districts from '../data/districts-2022.json' with { type: 'json' };
4
+ import parks from '../data/landmarks.json' with { type: 'json' };
5
+ import { createSFMapWithData } from './map-core.js';
6
+ const districtData = districts;
7
+ const parkData = parks;
8
+ const transitMapData = {
9
+ coast: coast.features[0].geometry,
10
+ districts: {
11
+ 2022: districtData.features.map(({ geometry, properties }) => ({
12
+ id: properties.district,
13
+ label: properties.label,
14
+ labelPoints: properties.labelPoints,
15
+ geometry,
16
+ extras: properties.displayExtras,
17
+ })),
18
+ },
19
+ landmarks: parkData.features.map(({ id, properties, geometry }) => ({
20
+ id,
21
+ ...properties,
22
+ geometry,
23
+ })),
24
+ };
3
25
  /** A deliberately schematic, offline station-to-station animation. Starts paused. */
4
26
  export function createTransitAnimation() {
5
27
  const host = Object.assign(document.createElement('div'), {
6
28
  destroy: () => { },
7
29
  });
8
30
  const root = host.attachShadow({ mode: 'open' });
9
- const map = createSFMap({ districtLabels: false, landmarks: true, labels: false });
31
+ const map = createSFMapWithData({ districtLabels: false, landmarks: true, labels: false }, transitMapData);
10
32
  root.innerHTML = `<style>
11
33
  :host{display:block;font:14px system-ui;color:#244747;max-width:800px}
12
34
  svg{display:block;width:100%;height:auto} .controls{display:flex;gap:12px;align-items:center;padding:12px;flex-wrap:wrap}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "1.3.8",
3
+ "version": "1.4.0",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -18,6 +18,50 @@
18
18
  "types": "./dist/data/index.d.ts",
19
19
  "import": "./dist/data/index.js"
20
20
  },
21
+ "./data/analysis": {
22
+ "types": "./dist/data/analysis.d.ts",
23
+ "import": "./dist/data/analysis.js"
24
+ },
25
+ "./data/catalog": {
26
+ "types": "./dist/data/catalog.d.ts",
27
+ "import": "./dist/data/catalog.js"
28
+ },
29
+ "./data/coast": {
30
+ "types": "./dist/data/coast.d.ts",
31
+ "import": "./dist/data/coast.js"
32
+ },
33
+ "./data/districts": {
34
+ "types": "./dist/data/districts.d.ts",
35
+ "import": "./dist/data/districts.js"
36
+ },
37
+ "./data/highways": {
38
+ "types": "./dist/data/highways.d.ts",
39
+ "import": "./dist/data/highways.js"
40
+ },
41
+ "./data/landmarks": {
42
+ "types": "./dist/data/landmarks.d.ts",
43
+ "import": "./dist/data/landmarks.js"
44
+ },
45
+ "./data/lookup": {
46
+ "types": "./dist/data/lookup.d.ts",
47
+ "import": "./dist/data/lookup.js"
48
+ },
49
+ "./data/realtor": {
50
+ "types": "./dist/data/realtor.d.ts",
51
+ "import": "./dist/data/realtor.js"
52
+ },
53
+ "./data/roads": {
54
+ "types": "./dist/data/roads.d.ts",
55
+ "import": "./dist/data/roads.js"
56
+ },
57
+ "./data/sf-find": {
58
+ "types": "./dist/data/sf-find.d.ts",
59
+ "import": "./dist/data/sf-find.js"
60
+ },
61
+ "./data/stations": {
62
+ "types": "./dist/data/stations.d.ts",
63
+ "import": "./dist/data/stations.js"
64
+ },
21
65
  "./data/*.json": "./dist/data/*.json",
22
66
  "./geometry": {
23
67
  "types": "./dist/src/geometry.d.ts",
@@ -39,6 +83,18 @@
39
83
  "types": "./dist/src/guide.d.ts",
40
84
  "import": "./dist/src/guide.js"
41
85
  },
86
+ "./guide/data": {
87
+ "types": "./dist/src/guide-data.d.ts",
88
+ "import": "./dist/src/guide-data.js"
89
+ },
90
+ "./guide/detailed": {
91
+ "types": "./dist/src/guide-detailed.d.ts",
92
+ "import": "./dist/src/guide-detailed.js"
93
+ },
94
+ "./guide/map": {
95
+ "types": "./dist/src/guide-map.d.ts",
96
+ "import": "./dist/src/guide-map.js"
97
+ },
42
98
  "./transit": {
43
99
  "types": "./dist/src/transit.d.ts",
44
100
  "import": "./dist/src/transit.js"