@fieldia/map 0.17.0 → 0.19.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 (154) hide show
  1. package/README.md +75 -10
  2. package/package.json +4 -4
  3. package/src/index.d.mts +5 -0
  4. package/src/index.d.ts +5 -0
  5. package/src/index.js +4 -1
  6. package/src/index.js.map +1 -1
  7. package/src/index.mjs +2 -0
  8. package/src/index.mjs.map +1 -1
  9. package/src/lib/address-labels.d.mts +21 -0
  10. package/src/lib/address-labels.d.ts +21 -0
  11. package/src/lib/address-labels.js +16 -0
  12. package/src/lib/address-labels.js.map +1 -0
  13. package/src/lib/address-labels.mjs +12 -0
  14. package/src/lib/address-labels.mjs.map +1 -0
  15. package/src/lib/address-line.d.mts +2 -0
  16. package/src/lib/address-line.d.ts +2 -0
  17. package/src/lib/address-line.js +34 -0
  18. package/src/lib/address-line.js.map +1 -0
  19. package/src/lib/address-line.mjs +31 -0
  20. package/src/lib/address-line.mjs.map +1 -0
  21. package/src/lib/address-parts.d.mts +28 -0
  22. package/src/lib/address-parts.d.ts +28 -0
  23. package/src/lib/address-parts.js +47 -0
  24. package/src/lib/address-parts.js.map +1 -0
  25. package/src/lib/address-parts.mjs +38 -0
  26. package/src/lib/address-parts.mjs.map +1 -0
  27. package/src/lib/address-search.d.mts +21 -0
  28. package/src/lib/address-search.d.ts +21 -0
  29. package/src/lib/address-search.js +52 -0
  30. package/src/lib/address-search.js.map +1 -0
  31. package/src/lib/address-search.mjs +49 -0
  32. package/src/lib/address-search.mjs.map +1 -0
  33. package/src/lib/address-suggest.d.mts +18 -0
  34. package/src/lib/address-suggest.d.ts +18 -0
  35. package/src/lib/address-suggest.js +140 -0
  36. package/src/lib/address-suggest.js.map +1 -0
  37. package/src/lib/address-suggest.mjs +137 -0
  38. package/src/lib/address-suggest.mjs.map +1 -0
  39. package/src/lib/area-count.d.mts +47 -0
  40. package/src/lib/area-count.d.ts +47 -0
  41. package/src/lib/area-count.js +123 -0
  42. package/src/lib/area-count.js.map +1 -0
  43. package/src/lib/area-count.mjs +116 -0
  44. package/src/lib/area-count.mjs.map +1 -0
  45. package/src/lib/card.d.mts +16 -6
  46. package/src/lib/card.d.ts +16 -6
  47. package/src/lib/card.js +71 -14
  48. package/src/lib/card.js.map +1 -1
  49. package/src/lib/card.mjs +70 -14
  50. package/src/lib/card.mjs.map +1 -1
  51. package/src/lib/cluster.d.mts +6 -1
  52. package/src/lib/cluster.d.ts +6 -1
  53. package/src/lib/cluster.js +11 -5
  54. package/src/lib/cluster.js.map +1 -1
  55. package/src/lib/cluster.mjs +11 -5
  56. package/src/lib/cluster.mjs.map +1 -1
  57. package/src/lib/embed.d.mts +86 -0
  58. package/src/lib/embed.d.ts +86 -0
  59. package/src/lib/embed.js +96 -0
  60. package/src/lib/embed.js.map +1 -0
  61. package/src/lib/embed.mjs +93 -0
  62. package/src/lib/embed.mjs.map +1 -0
  63. package/src/lib/form-map.d.mts +2 -1
  64. package/src/lib/form-map.d.ts +2 -1
  65. package/src/lib/form-map.js +159 -20
  66. package/src/lib/form-map.js.map +1 -1
  67. package/src/lib/form-map.mjs +161 -22
  68. package/src/lib/form-map.mjs.map +1 -1
  69. package/src/lib/kinds.d.mts +6 -4
  70. package/src/lib/kinds.d.ts +6 -4
  71. package/src/lib/kinds.js +49 -2
  72. package/src/lib/kinds.js.map +1 -1
  73. package/src/lib/kinds.mjs +49 -2
  74. package/src/lib/kinds.mjs.map +1 -1
  75. package/src/lib/labels.d.mts +5 -0
  76. package/src/lib/labels.d.ts +5 -0
  77. package/src/lib/labels.js +12 -4
  78. package/src/lib/labels.js.map +1 -1
  79. package/src/lib/labels.mjs +12 -4
  80. package/src/lib/labels.mjs.map +1 -1
  81. package/src/lib/leaflet.js +1 -0
  82. package/src/lib/leaflet.js.map +1 -1
  83. package/src/lib/leaflet.mjs +1 -0
  84. package/src/lib/leaflet.mjs.map +1 -1
  85. package/src/lib/list-view.d.mts +37 -1
  86. package/src/lib/list-view.d.ts +37 -1
  87. package/src/lib/list-view.js +103 -50
  88. package/src/lib/list-view.js.map +1 -1
  89. package/src/lib/list-view.mjs +103 -51
  90. package/src/lib/list-view.mjs.map +1 -1
  91. package/src/lib/map-core.d.mts +8 -3
  92. package/src/lib/map-core.d.ts +8 -3
  93. package/src/lib/map-core.js +21 -10
  94. package/src/lib/map-core.js.map +1 -1
  95. package/src/lib/map-core.mjs +21 -10
  96. package/src/lib/map-core.mjs.map +1 -1
  97. package/src/lib/marks.d.mts +11 -0
  98. package/src/lib/marks.d.ts +11 -0
  99. package/src/lib/marks.js +26 -1
  100. package/src/lib/marks.js.map +1 -1
  101. package/src/lib/marks.mjs +23 -0
  102. package/src/lib/marks.mjs.map +1 -1
  103. package/src/lib/pins.d.mts +20 -8
  104. package/src/lib/pins.d.ts +20 -8
  105. package/src/lib/pins.js +60 -15
  106. package/src/lib/pins.js.map +1 -1
  107. package/src/lib/pins.mjs +60 -17
  108. package/src/lib/pins.mjs.map +1 -1
  109. package/src/lib/place.js +2 -1
  110. package/src/lib/place.js.map +1 -1
  111. package/src/lib/place.mjs +2 -1
  112. package/src/lib/place.mjs.map +1 -1
  113. package/src/lib/providers.d.mts +31 -5
  114. package/src/lib/providers.d.ts +31 -5
  115. package/src/lib/providers.js +80 -58
  116. package/src/lib/providers.js.map +1 -1
  117. package/src/lib/providers.mjs +80 -58
  118. package/src/lib/providers.mjs.map +1 -1
  119. package/src/lib/regions.js +18 -1
  120. package/src/lib/regions.js.map +1 -1
  121. package/src/lib/regions.mjs +18 -1
  122. package/src/lib/regions.mjs.map +1 -1
  123. package/src/lib/search-box.d.mts +7 -1
  124. package/src/lib/search-box.d.ts +7 -1
  125. package/src/lib/search-box.js +15 -10
  126. package/src/lib/search-box.js.map +1 -1
  127. package/src/lib/search-box.mjs +15 -10
  128. package/src/lib/search-box.mjs.map +1 -1
  129. package/src/lib/settings.d.mts +19 -2
  130. package/src/lib/settings.d.ts +19 -2
  131. package/src/lib/styles.d.mts +13 -2
  132. package/src/lib/styles.d.ts +13 -2
  133. package/src/lib/styles.js +69 -4
  134. package/src/lib/styles.js.map +1 -1
  135. package/src/lib/styles.mjs +67 -3
  136. package/src/lib/styles.mjs.map +1 -1
  137. package/src/lib/tools.d.mts +6 -3
  138. package/src/lib/tools.d.ts +6 -3
  139. package/src/lib/tools.js +52 -1
  140. package/src/lib/tools.js.map +1 -1
  141. package/src/lib/tools.mjs +52 -1
  142. package/src/lib/tools.mjs.map +1 -1
  143. package/src/lib/view-box.d.mts +18 -0
  144. package/src/lib/view-box.d.ts +18 -0
  145. package/src/lib/view-box.js +31 -0
  146. package/src/lib/view-box.js.map +1 -0
  147. package/src/lib/view-box.mjs +25 -0
  148. package/src/lib/view-box.mjs.map +1 -0
  149. package/src/lib/widgets.d.mts +3 -0
  150. package/src/lib/widgets.d.ts +3 -0
  151. package/src/lib/widgets.js +10 -1
  152. package/src/lib/widgets.js.map +1 -1
  153. package/src/lib/widgets.mjs +10 -1
  154. package/src/lib/widgets.mjs.map +1 -1
@@ -1,8 +1,9 @@
1
1
  import type { Locale } from '@fieldia/core';
2
2
  import { type Fetch } from './answers.mjs';
3
+ import type { AddressParts } from './address-parts.mjs';
3
4
  import type { Bounds, LatLng } from './geo.mjs';
4
5
  /**
5
- * Where a map's images, credits, address search and routes come from. Every map in
6
+ * Where a map's images, live traffic, credits, address search and routes come from. Every map in
6
7
  * the package draws through one of these, so an app changes provider in one
7
8
  * place and nothing else moves. Keys never live in Fieldia: the app passes
8
9
  * them, or a function that fetches a short-lived token from its own server.
@@ -13,6 +14,10 @@ export interface TileSpec {
13
14
  /** With `{z}`, `{x}` and `{y}` where the tile's numbers go. */
14
15
  url: string;
15
16
  maxZoom: number;
17
+ /** The zoom they start at, when not the world's. */
18
+ minZoom?: number;
19
+ /** Seconds after which they are out of date, as live traffic's are: drawn again then, each address with `&t=` and the time. */
20
+ fresh?: number;
16
21
  tileSize?: number;
17
22
  zoomOffset?: number;
18
23
  /** Headers each image is fetched with: a token that must not sit in an address. */
@@ -38,7 +43,10 @@ export interface PlaceFound {
38
43
  lat: number;
39
44
  lng: number;
40
45
  address?: string;
46
+ parts?: AddressParts;
41
47
  }>;
48
+ /** Its address in its parts, when the search asked for them (`parts`); for Google's, `locate` gives them. */
49
+ parts?: AddressParts;
42
50
  }
43
51
  export interface SearchRequest {
44
52
  text: string;
@@ -48,6 +56,10 @@ export interface SearchRequest {
48
56
  zoom?: number;
49
57
  locale: Locale;
50
58
  signal?: AbortSignal;
59
+ /** Only places in these countries, by their ISO codes ("EG"). */
60
+ countries?: string[];
61
+ /** Each place's address in its parts too, for an address field to fill. */
62
+ parts?: boolean;
51
63
  }
52
64
  export type AddressSearch = (request: SearchRequest) => Promise<PlaceFound[]>;
53
65
  export type ReverseSearch = (at: LatLng & {
@@ -85,8 +97,10 @@ export interface MapProvider {
85
97
  readonly id: string;
86
98
  /** The map images of a kind, or null when the provider has none of it. */
87
99
  tiles(kind: BaseMap, locale: Locale): Promise<TileSpec | null>;
88
- /** What must be credited under the map, for what is in view. */
89
- credits(kind: BaseMap, view: MapView, locale: Locale): Promise<Credit[]>;
100
+ /** What must be credited under the map, for what is in view: its images', and its traffic's while that is drawn. */
101
+ credits(kind: BaseMap | 'traffic', view: MapView, locale: Locale): Promise<Credit[]>;
102
+ /** Live traffic, drawn over its map images of a kind; a provider with none has no Traffic button. */
103
+ traffic?(kind: BaseMap, locale: Locale): Promise<TileSpec | null>;
90
104
  /** The provider's mark, which its terms ask for in the map's corner. */
91
105
  readonly logo?: {
92
106
  text: string;
@@ -99,6 +113,8 @@ export interface MapProvider {
99
113
  readonly searchAsYouType?: boolean;
100
114
  /** Free for trying things out, not for an app in use: a warning says so once. */
101
115
  readonly demoOnly?: boolean;
116
+ /** What its terms ask to be said under its suggestions where no map of its own shows them; its mark (`logo`) says it for Google and Mapbox. */
117
+ readonly searchCredit?: Credit;
102
118
  /** A route along the roads, for a map's routes. */
103
119
  readonly route?: RouteFinder;
104
120
  /** The area within some minutes of a place, for a map's reachable area. */
@@ -135,7 +151,7 @@ export interface AzureMapsOptions {
135
151
  clientId?: string;
136
152
  fetch?: Fetch;
137
153
  }
138
- /** Microsoft Azure Maps: road, dark and satellite map images, credits for the view, and address search. */
154
+ /** Microsoft Azure Maps: road, dark and satellite map images, live traffic, credits for the view, and address search. */
139
155
  export declare function azureMaps(options: AzureMapsOptions): MapProvider;
140
156
  export interface GoogleMapsOptions {
141
157
  key: string;
@@ -148,7 +164,8 @@ export interface GoogleMapsOptions {
148
164
  /**
149
165
  * Google's map images through its Map Tiles API (a session for each kind and
150
166
  * language, credits for the view, Google's mark in the corner its terms
151
- * name), its Places API's suggestions as people type, and its Geocoding API
167
+ * name; its traffic, a layer of its own over them), its Places API's
168
+ * suggestions as people type, and its Geocoding API
152
169
  * to name a point. The key needs those three APIs enabled, and should be
153
170
  * restricted to the app's sites.
154
171
  */
@@ -159,6 +176,13 @@ export interface MapboxOptions {
159
176
  style?: string;
160
177
  darkStyle?: string;
161
178
  satelliteStyle?: string;
179
+ /**
180
+ * A style of the app's own that draws Mapbox Traffic's lines and nothing
181
+ * else (no background), for the Traffic button: Mapbox's traffic is a
182
+ * vector tileset its styles draw, and it has no such style of its own.
183
+ * Without one, no Traffic button.
184
+ */
185
+ trafficStyle?: string;
162
186
  /** Mapbox's wordmark, as an image the app serves; its name is shown until one is given. */
163
187
  logo?: string;
164
188
  /**
@@ -176,6 +200,8 @@ export interface TilesOptions {
176
200
  url: string;
177
201
  darkUrl?: string;
178
202
  satelliteUrl?: string;
203
+ /** The app's own live traffic, drawn over the map: with it, a Traffic button. */
204
+ trafficUrl?: string;
179
205
  credit: Credit | Credit[];
180
206
  maxZoom?: number;
181
207
  tileSize?: number;
@@ -1,8 +1,9 @@
1
1
  import type { Locale } from '@fieldia/core';
2
2
  import { type Fetch } from './answers';
3
+ import type { AddressParts } from './address-parts';
3
4
  import type { Bounds, LatLng } from './geo';
4
5
  /**
5
- * Where a map's images, credits, address search and routes come from. Every map in
6
+ * Where a map's images, live traffic, credits, address search and routes come from. Every map in
6
7
  * the package draws through one of these, so an app changes provider in one
7
8
  * place and nothing else moves. Keys never live in Fieldia: the app passes
8
9
  * them, or a function that fetches a short-lived token from its own server.
@@ -13,6 +14,10 @@ export interface TileSpec {
13
14
  /** With `{z}`, `{x}` and `{y}` where the tile's numbers go. */
14
15
  url: string;
15
16
  maxZoom: number;
17
+ /** The zoom they start at, when not the world's. */
18
+ minZoom?: number;
19
+ /** Seconds after which they are out of date, as live traffic's are: drawn again then, each address with `&t=` and the time. */
20
+ fresh?: number;
16
21
  tileSize?: number;
17
22
  zoomOffset?: number;
18
23
  /** Headers each image is fetched with: a token that must not sit in an address. */
@@ -38,7 +43,10 @@ export interface PlaceFound {
38
43
  lat: number;
39
44
  lng: number;
40
45
  address?: string;
46
+ parts?: AddressParts;
41
47
  }>;
48
+ /** Its address in its parts, when the search asked for them (`parts`); for Google's, `locate` gives them. */
49
+ parts?: AddressParts;
42
50
  }
43
51
  export interface SearchRequest {
44
52
  text: string;
@@ -48,6 +56,10 @@ export interface SearchRequest {
48
56
  zoom?: number;
49
57
  locale: Locale;
50
58
  signal?: AbortSignal;
59
+ /** Only places in these countries, by their ISO codes ("EG"). */
60
+ countries?: string[];
61
+ /** Each place's address in its parts too, for an address field to fill. */
62
+ parts?: boolean;
51
63
  }
52
64
  export type AddressSearch = (request: SearchRequest) => Promise<PlaceFound[]>;
53
65
  export type ReverseSearch = (at: LatLng & {
@@ -85,8 +97,10 @@ export interface MapProvider {
85
97
  readonly id: string;
86
98
  /** The map images of a kind, or null when the provider has none of it. */
87
99
  tiles(kind: BaseMap, locale: Locale): Promise<TileSpec | null>;
88
- /** What must be credited under the map, for what is in view. */
89
- credits(kind: BaseMap, view: MapView, locale: Locale): Promise<Credit[]>;
100
+ /** What must be credited under the map, for what is in view: its images', and its traffic's while that is drawn. */
101
+ credits(kind: BaseMap | 'traffic', view: MapView, locale: Locale): Promise<Credit[]>;
102
+ /** Live traffic, drawn over its map images of a kind; a provider with none has no Traffic button. */
103
+ traffic?(kind: BaseMap, locale: Locale): Promise<TileSpec | null>;
90
104
  /** The provider's mark, which its terms ask for in the map's corner. */
91
105
  readonly logo?: {
92
106
  text: string;
@@ -99,6 +113,8 @@ export interface MapProvider {
99
113
  readonly searchAsYouType?: boolean;
100
114
  /** Free for trying things out, not for an app in use: a warning says so once. */
101
115
  readonly demoOnly?: boolean;
116
+ /** What its terms ask to be said under its suggestions where no map of its own shows them; its mark (`logo`) says it for Google and Mapbox. */
117
+ readonly searchCredit?: Credit;
102
118
  /** A route along the roads, for a map's routes. */
103
119
  readonly route?: RouteFinder;
104
120
  /** The area within some minutes of a place, for a map's reachable area. */
@@ -135,7 +151,7 @@ export interface AzureMapsOptions {
135
151
  clientId?: string;
136
152
  fetch?: Fetch;
137
153
  }
138
- /** Microsoft Azure Maps: road, dark and satellite map images, credits for the view, and address search. */
154
+ /** Microsoft Azure Maps: road, dark and satellite map images, live traffic, credits for the view, and address search. */
139
155
  export declare function azureMaps(options: AzureMapsOptions): MapProvider;
140
156
  export interface GoogleMapsOptions {
141
157
  key: string;
@@ -148,7 +164,8 @@ export interface GoogleMapsOptions {
148
164
  /**
149
165
  * Google's map images through its Map Tiles API (a session for each kind and
150
166
  * language, credits for the view, Google's mark in the corner its terms
151
- * name), its Places API's suggestions as people type, and its Geocoding API
167
+ * name; its traffic, a layer of its own over them), its Places API's
168
+ * suggestions as people type, and its Geocoding API
152
169
  * to name a point. The key needs those three APIs enabled, and should be
153
170
  * restricted to the app's sites.
154
171
  */
@@ -159,6 +176,13 @@ export interface MapboxOptions {
159
176
  style?: string;
160
177
  darkStyle?: string;
161
178
  satelliteStyle?: string;
179
+ /**
180
+ * A style of the app's own that draws Mapbox Traffic's lines and nothing
181
+ * else (no background), for the Traffic button: Mapbox's traffic is a
182
+ * vector tileset its styles draw, and it has no such style of its own.
183
+ * Without one, no Traffic button.
184
+ */
185
+ trafficStyle?: string;
162
186
  /** Mapbox's wordmark, as an image the app serves; its name is shown until one is given. */
163
187
  logo?: string;
164
188
  /**
@@ -176,6 +200,8 @@ export interface TilesOptions {
176
200
  url: string;
177
201
  darkUrl?: string;
178
202
  satelliteUrl?: string;
203
+ /** The app's own live traffic, drawn over the map: with it, a Traffic button. */
204
+ trafficUrl?: string;
179
205
  credit: Credit | Credit[];
180
206
  maxZoom?: number;
181
207
  tileSize?: number;
@@ -6,8 +6,11 @@ exports.googleMaps = googleMaps;
6
6
  exports.mapbox = mapbox;
7
7
  exports.tiles = tiles;
8
8
  const answers_1 = require("./answers");
9
+ const address_line_1 = require("./address-line");
9
10
  /** The providers' routes, loaded the first time one is asked for: a form with only a place or an area never carries them. */
10
11
  const routes = () => Promise.resolve().then(() => require('./routes'));
12
+ /** How each provider's addresses read in their parts, loaded the first time a search asks for parts or countries (an address field's): a place's search never carries it. */
13
+ const partsReader = (parts, countries) => (parts || countries?.length ? Promise.resolve().then(() => require('./address-parts')) : undefined);
11
14
  const query = (params) => Object.entries(params)
12
15
  .filter(([, value]) => value !== undefined)
13
16
  .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
@@ -20,16 +23,8 @@ const textOfHtml = (html) => html
20
23
  .replace(/<[^>]*>/g, '')
21
24
  .replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (all, name) => name[0] === '#' ? String.fromCodePoint(parseInt(name.slice(name[1] === 'x' || name[1] === 'X' ? 2 : 1), name[1] === 'x' || name[1] === 'X' ? 16 : 10)) : (ENTITIES[name.toLowerCase()] ?? all))
22
25
  .trim();
23
- /** The words of a list that are there, once each, joined: "Downtown, Cairo, Egypt". */
24
- const joined = (...parts) => {
25
- const seen = [];
26
- for (const part of parts)
27
- if (typeof part === 'string' && part.trim() && !seen.includes(part.trim()))
28
- seen.push(part.trim());
29
- return seen.join(', ');
30
- };
31
- /** Places from a GeoJSON FeatureCollection: each named by the first of `labels` it has, with the line under it from `detail`. */
32
- function placesFrom(collection, labels, detail) {
26
+ /** Places from a GeoJSON FeatureCollection: each named by the first of `labels` it has, with the line under it from `detail`, and its parts from `parts`. */
27
+ function placesFrom(collection, labels, detail, parts) {
33
28
  const features = (0, answers_1.pathOf)(collection, 'features');
34
29
  if (!Array.isArray(features))
35
30
  return [];
@@ -39,13 +34,14 @@ function placesFrom(collection, labels, detail) {
39
34
  if (typeof lat !== 'number' || typeof lng !== 'number' || typeof label !== 'string')
40
35
  return [];
41
36
  const under = detail?.((0, answers_1.pathOf)(feature, 'properties'));
42
- return [{ label, ...(under && under !== label ? { detail: under } : {}), lat, lng }];
37
+ const found = parts?.((0, answers_1.pathOf)(feature, 'properties'));
38
+ return [{ label, ...(under && under !== label ? { detail: under } : {}), lat, lng, ...(found ? { parts: found } : {}) }];
43
39
  });
44
40
  }
45
41
  /** Languages Photon names places in; in any other it gives the places' own names. */
46
42
  const PHOTON_LANGUAGES = new Set(['en', 'de', 'fr']);
47
- /** Photon's places: each one's name (or its street and number) on one line, where it is on the next. */
48
- function photonPlaces(found) {
43
+ /** Photon's places: each one's name (or its street and number) on one line, where it is on the next; their parts when asked. */
44
+ function photonPlaces(found, locale, parts) {
49
45
  const features = (0, answers_1.pathOf)(found, 'features');
50
46
  if (!Array.isArray(features))
51
47
  return [];
@@ -57,8 +53,9 @@ function photonPlaces(found) {
57
53
  if (typeof lat !== 'number' || typeof lng !== 'number' || !label)
58
54
  return [];
59
55
  // Under a named place, its street when it has one; then its district, city and country.
60
- const detail = joined(label === streetOf(p) ? '' : streetOf(p), (0, answers_1.pathOf)(p, 'district'), (0, answers_1.pathOf)(p, 'city'), (0, answers_1.pathOf)(p, 'state'), (0, answers_1.pathOf)(p, 'country'));
61
- return [{ label, ...(detail ? { detail } : {}), lat, lng }];
56
+ const detail = (0, address_line_1.addressLine)([label === streetOf(p) ? '' : streetOf(p), (0, answers_1.pathOf)(p, 'district'), (0, answers_1.pathOf)(p, 'city'), (0, answers_1.pathOf)(p, 'state'), (0, answers_1.pathOf)(p, 'country')], locale);
57
+ const own = parts?.(p);
58
+ return [{ label, ...(detail ? { detail } : {}), lat, lng, ...(own ? { parts: own } : {}) }];
62
59
  });
63
60
  }
64
61
  /**
@@ -73,6 +70,7 @@ function openStreetMap(options = {}) {
73
70
  const base = {
74
71
  id: 'openstreetmap',
75
72
  demoOnly: true,
73
+ searchCredit: { text: '© OpenStreetMap contributors', href: 'https://www.openstreetmap.org/copyright' },
76
74
  async route(request) {
77
75
  return (await routes()).osrmRoute(fetchWith, osrm, request);
78
76
  },
@@ -87,8 +85,9 @@ function openStreetMap(options = {}) {
87
85
  return {
88
86
  ...base,
89
87
  searchAsYouType: false,
90
- async search({ text, locale, signal }) {
91
- const found = await (0, answers_1.getJson)(fetchWith, `https://nominatim.openstreetmap.org/search?${query({ format: 'jsonv2', limit: 5, q: text, 'accept-language': locale })}`, { signal });
88
+ async search({ text, locale, signal, countries, parts }) {
89
+ const found = await (0, answers_1.getJson)(fetchWith, `https://nominatim.openstreetmap.org/search?${query({ format: 'jsonv2', limit: 5, q: text, 'accept-language': locale, countrycodes: countries?.join(',').toLowerCase() || undefined, addressdetails: parts ? 1 : undefined })}`, { signal });
90
+ const read = await partsReader(parts);
92
91
  return Array.isArray(found)
93
92
  ? found.flatMap((place) => {
94
93
  const lat = Number((0, answers_1.pathOf)(place, 'lat'));
@@ -99,14 +98,15 @@ function openStreetMap(options = {}) {
99
98
  const name = (0, answers_1.pathOf)(place, 'name');
100
99
  const label = typeof name === 'string' && name.trim() ? name : whole.split(',')[0];
101
100
  const rest = whole.split(',').map((part) => part.trim()).filter((part, i) => !(i === 0 && part === label));
102
- return [{ label, ...(rest.length ? { detail: rest.join(', ') } : {}), lat, lng }];
101
+ const own = read?.nominatimParts((0, answers_1.pathOf)(place, 'address'));
102
+ const detail = (0, address_line_1.addressLine)(rest, locale);
103
+ return [{ label, ...(detail ? { detail } : {}), lat, lng, ...(own ? { parts: own } : {}) }];
103
104
  })
104
105
  : [];
105
106
  },
106
107
  async reverse({ lat, lng, locale, signal }) {
107
108
  const found = await (0, answers_1.getJson)(fetchWith, `https://nominatim.openstreetmap.org/reverse?${query({ format: 'jsonv2', lat, lon: lng, 'accept-language': locale })}`, { signal });
108
- const label = (0, answers_1.pathOf)(found, 'display_name');
109
- return typeof label === 'string' ? label : null;
109
+ return (0, address_line_1.addressLine)([(0, answers_1.pathOf)(found, 'display_name')], locale) || null;
110
110
  },
111
111
  };
112
112
  }
@@ -114,14 +114,16 @@ function openStreetMap(options = {}) {
114
114
  return {
115
115
  ...base,
116
116
  searchAsYouType: true,
117
- async search({ text, near, zoom, locale, signal }) {
117
+ async search({ text, near, zoom, locale, signal, countries, parts }) {
118
118
  const found = await (0, answers_1.getJson)(fetchWith, `${photon}/api?${query({ q: text, limit: 6, lang: lang(locale), lat: near?.lat, lon: near?.lng, zoom: near && zoom !== undefined ? Math.round(zoom) : undefined })}`, { signal });
119
- return photonPlaces(found);
119
+ // Photon cannot be asked to keep to some countries: those outside them are left out here.
120
+ const read = await partsReader(parts, countries);
121
+ return read ? read.inCountries(photonPlaces(found, locale, read.photonParts), countries) : photonPlaces(found, locale);
120
122
  },
121
123
  async reverse({ lat, lng, locale, signal }) {
122
124
  const found = await (0, answers_1.getJson)(fetchWith, `${photon}/reverse?${query({ lat, lon: lng, limit: 1, lang: lang(locale) })}`, { signal });
123
- const [place] = photonPlaces(found);
124
- return place ? joined(place.label, place.detail) : null;
125
+ const [place] = photonPlaces(found, locale);
126
+ return place ? (0, address_line_1.addressLine)([place.label, place.detail], locale) : null;
125
127
  },
126
128
  };
127
129
  }
@@ -133,8 +135,8 @@ function streetOf(properties) {
133
135
  }
134
136
  /** Azure lists the languages it draws; it refuses any other tag, `ar-001` among them. */
135
137
  const AZURE_LANGUAGE = { en: 'en-US', ar: 'ar', fr: 'fr-FR', de: 'de-DE' };
136
- const AZURE_TILESET = { road: 'microsoft.base.road', dark: 'microsoft.base.darkgrey', satellite: 'microsoft.imagery' };
137
- /** Microsoft Azure Maps: road, dark and satellite map images, credits for the view, and address search. */
138
+ const AZURE_TILESET = { road: 'microsoft.base.road', dark: 'microsoft.base.darkgrey', satellite: 'microsoft.imagery', traffic: 'microsoft.traffic.relative.main' };
139
+ /** Microsoft Azure Maps: road, dark and satellite map images, live traffic, credits for the view, and address search. */
138
140
  function azureMaps(options) {
139
141
  if (!options.key && !options.token)
140
142
  throw new Error('azureMaps needs a key or a token');
@@ -145,12 +147,16 @@ function azureMaps(options) {
145
147
  signal,
146
148
  headers: { ...(await headers()), ...(locale ? { 'Accept-Language': AZURE_LANGUAGE[locale] } : {}) },
147
149
  });
150
+ const tile = (tileset, locale) => `https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=${tileset}&zoom={z}&x={x}&y={y}&tileSize=256&language=${AZURE_LANGUAGE[locale]}&view=Auto${auth}`;
148
151
  return {
149
152
  id: 'azure-maps',
150
153
  searchAsYouType: true,
151
154
  async tiles(kind, locale) {
152
- const url = `https://atlas.microsoft.com/map/tile?api-version=2024-04-01&tilesetId=${AZURE_TILESET[kind]}&zoom={z}&x={x}&y={y}&tileSize=256&language=${AZURE_LANGUAGE[locale]}&view=Auto${auth}`;
153
- return { url, maxZoom: kind === 'satellite' ? 19 : 22, ...(options.key ? {} : { headers }) };
155
+ return { url: tile(AZURE_TILESET[kind], locale), maxZoom: kind === 'satellite' ? 19 : 22, ...(options.key ? {} : { headers }) };
156
+ },
157
+ // How each road flows against its free flow, in its own colours over the dark map; zooms 6 to 21 (it answers none outside them), out of date in four minutes (its cache-control's max-age).
158
+ async traffic(kind, locale) {
159
+ return { url: tile(kind === 'dark' ? 'microsoft.traffic.relative.dark' : AZURE_TILESET.traffic, locale), minZoom: 6, maxZoom: 21, fresh: 240, ...(options.key ? {} : { headers }) };
154
160
  },
155
161
  async credits(kind, { zoom, bounds }) {
156
162
  try {
@@ -164,15 +170,17 @@ function azureMaps(options) {
164
170
  }
165
171
  return [{ text: '© Microsoft' }];
166
172
  },
167
- async search({ text, near, locale, signal }) {
173
+ async search({ text, near, locale, signal, countries, parts }) {
168
174
  const coordinates = near ? `${near.lng},${near.lat}` : undefined;
169
175
  // Its autocomplete reads a word half typed, as geocoding does not; its suggestions carry no place, so a picked one is geocoded then.
170
176
  const found = await ask('geocode:autocomplete', { 'api-version': '2026-01-01', top: 5, query: text, coordinates }, locale, signal);
171
- return azurePlaces(found).map((place) => ({
177
+ // Its parts come with each suggestion; those outside the countries asked for are left out here.
178
+ const read = await partsReader(parts, countries);
179
+ return (read ? read.inCountries(azurePlaces(found, locale, read.azureParts), countries) : azurePlaces(found, locale)).map((place) => ({
172
180
  ...place,
173
181
  locate: async () => {
174
- const address = [place.label, place.detail].filter(Boolean).join(', ');
175
- const [hit] = azurePlaces(await ask('geocode', { 'api-version': '2023-06-01', top: 1, query: place.whole ?? address, coordinates }, locale));
182
+ const address = (0, address_line_1.addressLine)([place.label, place.detail], locale);
183
+ const [hit] = azurePlaces(await ask('geocode', { 'api-version': '2023-06-01', top: 1, query: place.whole ?? address, coordinates }, locale), locale);
176
184
  if (hit?.lat === undefined || hit.lng === undefined)
177
185
  throw new Error('Azure Maps found no place');
178
186
  // The address as its two lines read: with its town, which Azure's own may leave out.
@@ -184,7 +192,7 @@ function azureMaps(options) {
184
192
  const found = await ask('reverseGeocode', { 'api-version': '2023-06-01', coordinates: `${lng},${lat}` }, locale, signal);
185
193
  const features = (0, answers_1.pathOf)(found, 'features');
186
194
  const formatted = Array.isArray(features) ? clean((0, answers_1.pathOf)(features[0], 'properties', 'address', 'formattedAddress')) : '';
187
- return formatted || null;
195
+ return (0, address_line_1.addressLine)([formatted], locale) || null;
188
196
  },
189
197
  async route(request) {
190
198
  return (await routes()).azureRoute({ fetchWith, auth, headers }, request);
@@ -200,7 +208,7 @@ function azureMaps(options) {
200
208
  * says (geocoding does, autocomplete does not); and the whole address as Azure
201
209
  * wrote it, to geocode a suggestion by.
202
210
  */
203
- function azurePlaces(found) {
211
+ function azurePlaces(found, locale, partsOf) {
204
212
  const features = (0, answers_1.pathOf)(found, 'features');
205
213
  if (!Array.isArray(features))
206
214
  return [];
@@ -220,14 +228,17 @@ function azurePlaces(found) {
220
228
  if (town && !rest.includes(town))
221
229
  rest = [town, ...rest];
222
230
  const at = typeof lat === 'number' && typeof lng === 'number' ? { lat, lng } : {};
223
- return [{ label, ...(rest.length ? { detail: rest.join(', ') } : {}), ...at, ...(whole ? { whole } : {}) }];
231
+ const own = partsOf?.(address);
232
+ const detail = (0, address_line_1.addressLine)(rest, locale);
233
+ return [{ label, ...(detail ? { detail } : {}), ...at, ...(whole ? { whole } : {}), ...(own ? { parts: own } : {}) }];
224
234
  });
225
235
  }
226
- const GOOGLE_MAP_TYPE = { road: 'roadmap', satellite: 'satellite' };
236
+ const GOOGLE_MAP_TYPE = { road: 'roadmap', satellite: 'satellite', traffic: 'roadmap' };
227
237
  /**
228
238
  * Google's map images through its Map Tiles API (a session for each kind and
229
239
  * language, credits for the view, Google's mark in the corner its terms
230
- * name), its Places API's suggestions as people type, and its Geocoding API
240
+ * name; its traffic, a layer of its own over them), its Places API's
241
+ * suggestions as people type, and its Geocoding API
231
242
  * to name a point. The key needs those three APIs enabled, and should be
232
243
  * restricted to the app's sites.
233
244
  */
@@ -237,14 +248,15 @@ function googleMaps(options) {
237
248
  const sessions = new Map();
238
249
  function session(kind, locale) {
239
250
  const mapType = GOOGLE_MAP_TYPE[kind];
240
- const id = `${mapType}:${locale}`;
251
+ const id = `${kind}:${locale}`;
241
252
  const known = sessions.get(id);
242
253
  return known ?? open();
243
254
  function open() {
244
255
  const opening = (0, answers_1.getJson)(fetchWith, `https://tile.googleapis.com/v1/createSession?key=${key}`, {
245
256
  method: 'POST',
246
257
  headers: { 'Content-Type': 'application/json' },
247
- body: JSON.stringify({ mapType, language: locale, region: options.region ?? 'US' }),
258
+ // Traffic alone, without the map under it: an overlay.
259
+ body: JSON.stringify({ mapType, language: locale, region: options.region ?? 'US', ...(kind === 'traffic' ? { layerTypes: ['layerTraffic'], overlay: true } : {}) }),
248
260
  }).then((answer) => {
249
261
  const value = (0, answers_1.pathOf)(answer, 'session');
250
262
  if (typeof value !== 'string')
@@ -266,13 +278,19 @@ function googleMaps(options) {
266
278
  /** One session from the first letter typed to the place picked: Google charges the suggestions of a session that ends in a pick as the pick. */
267
279
  let searching;
268
280
  const newSession = () => (globalThis.crypto?.randomUUID?.() ?? 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => ((Math.random() * 16) | (c === 'x' ? 0 : 8)).toString(16).slice(0, 1)));
281
+ async function tilesOf(kind, locale) {
282
+ if (!GOOGLE_MAP_TYPE[kind])
283
+ return null;
284
+ const { session: id } = await session(kind, locale);
285
+ return { url: `https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session=${id}&key=${key}`, maxZoom: 22 };
286
+ }
269
287
  /** How far "near" reaches at a zoom: about what the map shows, within Google's bounds. */
270
288
  const radiusAt = (zoom) => Math.round(Math.min(50_000, Math.max(100, 40_075_016 / 2 ** (zoom ?? 12))));
271
289
  return {
272
290
  id: 'google',
273
291
  logo: { text: 'Google Maps', ...(options.logo ? { image: options.logo } : {}) },
274
292
  searchAsYouType: true,
275
- async search({ text, near, zoom, locale, signal }) {
293
+ async search({ text, near, zoom, locale, signal, countries, parts }) {
276
294
  searching ??= newSession();
277
295
  const token = searching;
278
296
  const answer = await (0, answers_1.getJson)(fetchWith, 'https://places.googleapis.com/v1/places:autocomplete', {
@@ -283,6 +301,7 @@ function googleMaps(options) {
283
301
  input: text,
284
302
  languageCode: locale,
285
303
  ...(options.region ? { regionCode: options.region } : {}),
304
+ ...(countries?.length ? { includedRegionCodes: countries.map((code) => code.toLowerCase()) } : {}),
286
305
  sessionToken: token,
287
306
  ...(near ? { locationBias: { circle: { center: { latitude: near.lat, longitude: near.lng }, radius: radiusAt(zoom) } } } : {}),
288
307
  }),
@@ -296,14 +315,15 @@ function googleMaps(options) {
296
315
  const label = clean((0, answers_1.pathOf)(prediction, 'structuredFormat', 'mainText', 'text')) || clean((0, answers_1.pathOf)(prediction, 'text', 'text'));
297
316
  if (typeof placeId !== 'string' || !label)
298
317
  return [];
299
- const detail = clean((0, answers_1.pathOf)(prediction, 'structuredFormat', 'secondaryText', 'text'));
318
+ const detail = (0, address_line_1.addressLine)([(0, answers_1.pathOf)(prediction, 'structuredFormat', 'secondaryText', 'text')], locale);
300
319
  return [
301
320
  {
302
321
  label,
303
322
  ...(detail ? { detail } : {}),
304
323
  locate: async () => {
305
324
  const place = await (0, answers_1.getJson)(fetchWith, `https://places.googleapis.com/v1/places/${encodeURIComponent(placeId)}?${query({ languageCode: locale, sessionToken: token })}`, {
306
- headers: { 'X-Goog-Api-Key': options.key, 'X-Goog-FieldMask': 'location,formattedAddress' },
325
+ // Its parts, when asked for, at the same price as the rest (Place Details Essentials).
326
+ headers: { 'X-Goog-Api-Key': options.key, 'X-Goog-FieldMask': `location,formattedAddress${parts ? ',addressComponents' : ''}` },
307
327
  });
308
328
  // Picked: the session is over, the next letter typed opens another.
309
329
  if (searching === token)
@@ -312,8 +332,9 @@ function googleMaps(options) {
312
332
  const longitude = (0, answers_1.pathOf)(place, 'location', 'longitude');
313
333
  if (typeof latitude !== 'number' || typeof longitude !== 'number')
314
334
  throw new Error('Google gave no place');
315
- const address = clean((0, answers_1.pathOf)(place, 'formattedAddress'));
316
- return { lat: latitude, lng: longitude, ...(address ? { address } : {}) };
335
+ const address = (0, address_line_1.addressLine)([(0, answers_1.pathOf)(place, 'formattedAddress')], locale);
336
+ const own = (await partsReader(parts))?.googleParts((0, answers_1.pathOf)(place, 'addressComponents'));
337
+ return { lat: latitude, lng: longitude, ...(address ? { address } : {}), ...(own ? { parts: own } : {}) };
317
338
  },
318
339
  },
319
340
  ];
@@ -326,18 +347,14 @@ function googleMaps(options) {
326
347
  if (typeof status === 'string' && status !== 'OK' && status !== 'ZERO_RESULTS')
327
348
  throw new Error(`Google's Geocoding API answered ${status}: ${String((0, answers_1.pathOf)(answer, 'error_message') ?? '')}`);
328
349
  const results = (0, answers_1.pathOf)(answer, 'results');
329
- const named = Array.isArray(results) ? clean((0, answers_1.pathOf)(results[0], 'formatted_address')) : '';
350
+ const named = Array.isArray(results) ? (0, address_line_1.addressLine)([(0, answers_1.pathOf)(results[0], 'formatted_address')], locale) : '';
330
351
  return named || null;
331
352
  },
332
353
  async route(request) {
333
354
  return (await routes()).googleRoute({ fetchWith, key: options.key }, request);
334
355
  },
335
- async tiles(kind, locale) {
336
- if (!GOOGLE_MAP_TYPE[kind])
337
- return null;
338
- const { session: id } = await session(kind, locale);
339
- return { url: `https://tile.googleapis.com/v1/2dtiles/{z}/{x}/{y}?session=${id}&key=${key}`, maxZoom: 22 };
340
- },
356
+ tiles: (kind, locale) => tilesOf(kind, locale),
357
+ traffic: (_kind, locale) => tilesOf('traffic', locale),
341
358
  async credits(kind, { zoom, bounds }, locale) {
342
359
  try {
343
360
  const { session: id } = await session(kind === 'dark' ? 'road' : kind, locale);
@@ -362,6 +379,8 @@ function mapbox(options) {
362
379
  dark: options.darkStyle ?? 'mapbox/dark-v11',
363
380
  satellite: options.satelliteStyle ?? 'mapbox/satellite-streets-v12',
364
381
  };
382
+ const styleTiles = (style) => ({ url: `https://api.mapbox.com/styles/v1/${style}/tiles/512/{z}/{x}/{y}@2x?access_token=${token}`, maxZoom: 22, tileSize: 512, zoomOffset: -1 });
383
+ const traffic = options.trafficStyle;
365
384
  const permanent = options.permanent ?? true;
366
385
  const ask = async (path, params, signal) => (0, answers_1.getJson)(fetchWith, `https://api.mapbox.com/search/geocode/v6/${path}?${query({ ...params, permanent: String(permanent) })}&access_token=${token}`, { signal });
367
386
  return {
@@ -369,8 +388,9 @@ function mapbox(options) {
369
388
  searchAsYouType: true,
370
389
  logo: { text: 'mapbox', ...(options.logo ? { image: options.logo } : {}), href: 'https://www.mapbox.com/' },
371
390
  async tiles(kind) {
372
- return { url: `https://api.mapbox.com/styles/v1/${styles[kind]}/tiles/512/{z}/{x}/{y}@2x?access_token=${token}`, maxZoom: 22, tileSize: 512, zoomOffset: -1 };
391
+ return styleTiles(styles[kind]);
373
392
  },
393
+ ...(traffic ? { traffic: async () => styleTiles(traffic) } : {}),
374
394
  async credits() {
375
395
  return [
376
396
  { text: '© Mapbox', href: 'https://www.mapbox.com/about/maps/' },
@@ -378,11 +398,12 @@ function mapbox(options) {
378
398
  { text: 'Improve this map', href: 'https://www.mapbox.com/map-feedback/' },
379
399
  ];
380
400
  },
381
- async search({ text, near, locale, signal }) {
382
- return placesFrom(await ask('forward', { q: text, limit: 5, language: locale, proximity: near ? `${near.lng},${near.lat}` : undefined }, signal), [['properties', 'name'], ['properties', 'full_address']], (p) => joined((0, answers_1.pathOf)(p, 'place_formatted')));
401
+ async search({ text, near, locale, signal, countries, parts }) {
402
+ return placesFrom(await ask('forward', { q: text, limit: 5, language: locale, proximity: near ? `${near.lng},${near.lat}` : undefined, country: countries?.join(',').toLowerCase() || undefined }, signal), [['properties', 'name'], ['properties', 'full_address']], (p) => (0, address_line_1.addressLine)([(0, answers_1.pathOf)(p, 'place_formatted')], locale), (await partsReader(parts))?.mapboxParts);
383
403
  },
384
404
  async reverse({ lat, lng, locale, signal }) {
385
- return placesFrom(await ask('reverse', { longitude: lng, latitude: lat, language: locale, limit: 1 }, signal), [['properties', 'full_address'], ['properties', 'name']])[0]?.label ?? null;
405
+ const [named] = placesFrom(await ask('reverse', { longitude: lng, latitude: lat, language: locale, limit: 1 }, signal), [['properties', 'full_address'], ['properties', 'name']]);
406
+ return named ? (0, address_line_1.addressLine)([named.label], locale) : null;
386
407
  },
387
408
  async route(request) {
388
409
  return (await routes()).mapboxRoute({ fetchWith, token }, request);
@@ -396,6 +417,8 @@ function mapbox(options) {
396
417
  function tiles(options) {
397
418
  const urls = { road: options.url, dark: options.darkUrl, satellite: options.satelliteUrl };
398
419
  const credits = Array.isArray(options.credit) ? options.credit : [options.credit];
420
+ const spec = (url) => ({ url, maxZoom: options.maxZoom ?? 19, ...(options.tileSize ? { tileSize: options.tileSize } : {}), ...(options.zoomOffset ? { zoomOffset: options.zoomOffset } : {}) });
421
+ const traffic = options.trafficUrl;
399
422
  return {
400
423
  id: 'tiles',
401
424
  search: options.search,
@@ -405,10 +428,9 @@ function tiles(options) {
405
428
  searchAsYouType: options.searchAsYouType ?? !!options.search,
406
429
  async tiles(kind) {
407
430
  const url = urls[kind];
408
- if (!url)
409
- return null;
410
- return { url, maxZoom: options.maxZoom ?? 19, ...(options.tileSize ? { tileSize: options.tileSize } : {}), ...(options.zoomOffset ? { zoomOffset: options.zoomOffset } : {}) };
431
+ return url ? spec(url) : null;
411
432
  },
433
+ ...(traffic ? { traffic: async () => spec(traffic) } : {}),
412
434
  async credits() {
413
435
  return credits;
414
436
  },