@chaosity/location-client 0.9.0 → 0.11.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 (50) hide show
  1. package/README.md +307 -37
  2. package/dist/adapters/GeoPlaces.d.ts +30 -3
  3. package/dist/adapters/GeoPlaces.js +5 -1
  4. package/dist/cjs/adapters/GeoPlaces.d.ts +30 -3
  5. package/dist/cjs/adapters/GeoPlaces.js +5 -4
  6. package/dist/cjs/client/GeoPlacesClient.d.ts +16 -4
  7. package/dist/cjs/client/GeoPlacesClient.js +18 -4
  8. package/dist/cjs/client/commands.d.ts +159 -0
  9. package/dist/cjs/client/commands.js +108 -0
  10. package/dist/cjs/errors/LocationServiceException.d.ts +28 -1
  11. package/dist/cjs/errors/LocationServiceException.js +31 -2
  12. package/dist/cjs/index.d.ts +5 -3
  13. package/dist/cjs/index.js +17 -1
  14. package/dist/cjs/maps/mapEnums.d.ts +82 -4
  15. package/dist/cjs/maps/mapEnums.js +98 -5
  16. package/dist/cjs/maps/mapStyle.d.ts +87 -10
  17. package/dist/cjs/maps/mapStyle.js +28 -5
  18. package/dist/cjs/maps/staticMap.d.ts +40 -0
  19. package/dist/cjs/maps/staticMap.js +4 -0
  20. package/dist/cjs/server/LocationServiceConnector.d.ts +23 -5
  21. package/dist/cjs/server/LocationServiceConnector.js +25 -5
  22. package/dist/cjs/transport/endpoints.js +3 -0
  23. package/dist/cjs/transport/http.d.ts +2 -2
  24. package/dist/cjs/transport/http.js +2 -2
  25. package/dist/cjs/types/index.d.ts +6 -5
  26. package/dist/cjs/utils/tokenClaims.d.ts +29 -3
  27. package/dist/cjs/utils/tokenClaims.js +28 -3
  28. package/dist/client/GeoPlacesClient.d.ts +16 -4
  29. package/dist/client/GeoPlacesClient.js +18 -4
  30. package/dist/client/commands.d.ts +159 -0
  31. package/dist/client/commands.js +97 -0
  32. package/dist/errors/LocationServiceException.d.ts +28 -1
  33. package/dist/errors/LocationServiceException.js +30 -1
  34. package/dist/index.d.ts +5 -3
  35. package/dist/index.js +7 -2
  36. package/dist/maps/mapEnums.d.ts +82 -4
  37. package/dist/maps/mapEnums.js +97 -4
  38. package/dist/maps/mapStyle.d.ts +87 -10
  39. package/dist/maps/mapStyle.js +28 -5
  40. package/dist/maps/staticMap.d.ts +40 -0
  41. package/dist/maps/staticMap.js +4 -0
  42. package/dist/server/LocationServiceConnector.d.ts +23 -5
  43. package/dist/server/LocationServiceConnector.js +25 -5
  44. package/dist/transport/endpoints.js +3 -0
  45. package/dist/transport/http.d.ts +2 -2
  46. package/dist/transport/http.js +2 -2
  47. package/dist/types/index.d.ts +6 -5
  48. package/dist/utils/tokenClaims.d.ts +29 -3
  49. package/dist/utils/tokenClaims.js +28 -3
  50. package/package.json +3 -3
package/README.md CHANGED
@@ -34,13 +34,20 @@ npm install maplibre-gl
34
34
  npm install maplibre-gl @maplibre/maplibre-gl-geocoder
35
35
  ```
36
36
 
37
+ The map helpers need `maplibre-gl` 6.4.1 or a later 6.x release. Earlier
38
+ releases carry
39
+ [GHSA-jrc7-96c5-q579](https://github.com/advisories/GHSA-jrc7-96c5-q579), an
40
+ XSS in the attribution control, and none of them has a fix. MapLibre 6 needs
41
+ its worker set up once under a bundler: see [The MapLibre worker](#the-maplibre-worker).
42
+
37
43
  ## Key Features
38
44
 
39
45
  - **Custom Authentication**: Uses Bearer tokens instead of AWS SigV4
40
46
  - **AWS SDK Commands**: Full access to all AWS Location Service commands
47
+ - **Address Verification**: `verifyAddress(placeId)` returns the one Places result you may store — see [Verifying an address](#verifying-an-address)
41
48
  - **Data Type Utilities**: Built-in GeoJSON conversion utilities
42
49
  - **MapLibre Integration**: Adapter for MapLibre GL Geocoder and `createTransformRequest` helper
43
- - **Map Style Control**: Fetch and customize map style descriptors with terrain, 3D buildings, traffic, and more
50
+ - **Map Style Control**: Fetch and customize map style descriptors — color scheme, points of interest, label language, and terrain, 3D buildings, traffic and more as [plan features](#plan-features)
44
51
  - **Map Language**: Switch map label language client-side with zero API calls
45
52
  - **POI Layer Control**: Toggle point-of-interest categories on/off by layer
46
53
  - **Server Utilities**: `getClientConfig()` with auto-env detection and token caching
@@ -101,10 +108,78 @@ const client = new GeoPlacesClient({
101
108
  })
102
109
 
103
110
  const response: SuggestCommandOutput = await client.send(
104
- new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 }),
111
+ new SuggestCommand({
112
+ QueryText: 'Vancouver',
113
+ MaxResults: 5,
114
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
115
+ BiasPosition: [-123.1207, 49.2827],
116
+ }),
105
117
  )
106
118
  ```
107
119
 
120
+ ### Verifying an address
121
+
122
+ `POST /address/verify` resolves one PlaceId to the full place record plus
123
+ `verified`. The PlaceId can come from any suggestion, autocomplete, geocode or
124
+ place result, a unit's included. The answer is **the one Places result you may
125
+ store**; every other result is for display only. The exception is a place in
126
+ Japan, which may not be stored at all.
127
+
128
+ The flow is suggest, then an optional unit pick, then verify at submit:
129
+
130
+ ```typescript
131
+ import {
132
+ GetPlaceCommand,
133
+ SuggestCommand,
134
+ type GetPlaceCommandOutput,
135
+ type SuggestCommandOutput,
136
+ } from '@chaosity/location-client'
137
+
138
+ // 1. While the person types: suggestions, for display.
139
+ const suggestions: SuggestCommandOutput = await client.send(
140
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
141
+ new SuggestCommand({
142
+ QueryText: '100 George St, Sydney',
143
+ BiasPosition: [151.2093, -33.8688],
144
+ }),
145
+ )
146
+ const picked = suggestions.ResultItems?.[0]?.Place?.PlaceId
147
+
148
+ // 2. Optionally, offer the building's units. Each has its own PlaceId.
149
+ const building: GetPlaceCommandOutput = await client.send(
150
+ new GetPlaceCommand({
151
+ PlaceId: picked!,
152
+ AdditionalFeatures: ['SecondaryAddresses'],
153
+ }),
154
+ )
155
+ const unit = building.SecondaryAddresses?.[0]?.PlaceId
156
+
157
+ // 3. At submit, once: verify the PlaceId that was chosen.
158
+ const answer = await client.verifyAddress(unit ?? picked!)
159
+ if (answer.verified) {
160
+ // `answer` is the record you may keep.
161
+ }
162
+ ```
163
+
164
+ - `verified` is `true` for a `PointAddress`, or a `SecondaryAddress` (a unit).
165
+ It is `false` for anything else: an interpolated address, a street, a
166
+ locality, a point of interest. A `false` is still a 200, so the call
167
+ resolves; it does not throw.
168
+ - **Every verify is billed, whether or not the address verifies.** Call it
169
+ once per chosen PlaceId, at submit. Never call it per keystroke, or on every
170
+ pick.
171
+ - Only the `PlaceId` is sent. `VerifyAddressCommandInput` has no other field,
172
+ because the service would drop `Language`, `PoliticalView` and
173
+ `AdditionalFeatures`. A repeat verify of the same PlaceId may be answered
174
+ from the service's own store, and is billed all the same.
175
+ - Keep the PlaceId you sent beside the answer. The answer's own `PlaceId` can
176
+ differ, and for a unit it does. The service does not accept that one back,
177
+ while the one you sent verifies again.
178
+ - `client.verifyAddress(placeId)` is
179
+ `client.send(new VerifyAddressCommand({ PlaceId: placeId }))`, typed as
180
+ `VerifyAddressResponse`. `connector.verifyAddress` does the same on the
181
+ server.
182
+
108
183
  ### MapLibre Map Integration
109
184
 
110
185
  Use `fetchMapStyle` to fetch a style descriptor with authentication and optional customization, and `createTransformRequest` to attach Bearer tokens to all subsequent tile/glyph/sprite requests:
@@ -114,12 +189,15 @@ import {
114
189
  fetchMapStyle,
115
190
  createTransformRequest,
116
191
  } from '@chaosity/location-client'
117
- import maplibregl from 'maplibre-gl'
192
+ import * as maplibregl from 'maplibre-gl'
193
+ import 'maplibre-gl/dist/maplibre-gl.css'
194
+ // Vite. For other bundlers, see "The MapLibre worker" below.
195
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'
196
+
197
+ maplibregl.setWorkerUrl(workerUrl)
118
198
 
119
199
  const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
120
200
  colorScheme: 'Dark',
121
- terrain: 'Terrain3D',
122
- buildings: 'Buildings3D',
123
201
  language: 'fr',
124
202
  })
125
203
 
@@ -128,11 +206,72 @@ const map = new maplibregl.Map({
128
206
  style,
129
207
  center: [-123.12, 49.28],
130
208
  zoom: 10,
131
- maxPitch: 85,
132
209
  transformRequest: createTransformRequest(apiUrl, getToken),
133
210
  })
134
211
  ```
135
212
 
213
+ 3D terrain and buildings need the `terrain` and `buildings` plan features — see [Plan features](#plan-features):
214
+
215
+ ```typescript
216
+ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
217
+ terrain: 'Terrain3D',
218
+ buildings: 'Buildings3D',
219
+ })
220
+ // then `maxPitch: 85` on the map, so the camera can tilt to see them
221
+ ```
222
+
223
+ ### The MapLibre worker
224
+
225
+ MapLibre 6 loads and parses its tiles in a Web Worker, and it finds the
226
+ worker's file from its own module URL. A bundler rewrites that URL, so an
227
+ application built with one sets the worker's URL once, before the first map.
228
+ Without it the map mounts, draws no tile, and logs "Worker failed to load".
229
+
230
+ With Vite, import the worker's URL, as in the example above:
231
+
232
+ ```typescript
233
+ import * as maplibregl from 'maplibre-gl'
234
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url'
235
+
236
+ maplibregl.setWorkerUrl(workerUrl)
237
+ ```
238
+
239
+ With Next.js, serve the worker and the chunk it imports from `public/`. Copy
240
+ them before every build and dev run:
241
+
242
+ ```js
243
+ // scripts/copy-maplibre-worker.mjs
244
+ import { copyFileSync, mkdirSync } from 'node:fs'
245
+ import { createRequire } from 'node:module'
246
+ import path from 'node:path'
247
+
248
+ const pkg = createRequire(import.meta.url).resolve('maplibre-gl/package.json')
249
+ const dist = path.join(path.dirname(pkg), 'dist')
250
+ const dest = path.join(process.cwd(), 'public', 'maplibre')
251
+ mkdirSync(dest, { recursive: true })
252
+ for (const file of ['maplibre-gl-worker.mjs', 'maplibre-gl-shared.mjs']) {
253
+ copyFileSync(path.join(dist, file), path.join(dest, file))
254
+ }
255
+ ```
256
+
257
+ ```json
258
+ "scripts": {
259
+ "predev": "node scripts/copy-maplibre-worker.mjs",
260
+ "prebuild": "node scripts/copy-maplibre-worker.mjs"
261
+ }
262
+ ```
263
+
264
+ Then, in the client component that builds the map:
265
+
266
+ ```typescript
267
+ import * as maplibregl from 'maplibre-gl'
268
+
269
+ maplibregl.setWorkerUrl('/maplibre/maplibre-gl-worker.mjs')
270
+ ```
271
+
272
+ Other bundlers, and loading MapLibre from a CDN, are covered in MapLibre's own
273
+ [installation guide](https://maplibre.org/maplibre-gl-js/docs/#installation).
274
+
136
275
  ### Switching Map Language
137
276
 
138
277
  Change map label language instantly on the client side — no API calls needed:
@@ -170,6 +309,74 @@ setAllPoiVisibility(map, false)
170
309
 
171
310
  Available categories: `food_drink`, `entertainment`, `sights`, `transit`, `accommodations`, `leisure`, `shopping`, `business`, `facilities`, `areas`, `parks`.
172
311
 
312
+ ### Plan features
313
+
314
+ Some options are features of the application's plan. An application whose plan
315
+ does not include one is refused **403 `FeatureNotEntitledException`** before
316
+ anything is fetched upstream, so the refusal is not billed, and the message
317
+ names each refused feature and the option that asked for it:
318
+
319
+ > This application's plan does not include the map feature terrain (terrain=Terrain3D).
320
+
321
+ | Feature | What asks for it |
322
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
323
+ | `satellite` | the `Satellite` and `Hybrid` styles, and a static map with no `style` — Satellite is its default |
324
+ | `terrain` | `terrain` (`Hillshade`, `Terrain3D`) |
325
+ | `buildings` | `buildings` |
326
+ | `contours` | `contourDensity` |
327
+ | `traffic` | `traffic` |
328
+ | `travel-modes` | `travelModes` |
329
+ | `political-view` | `politicalView`, on a style or a static map |
330
+ | `rich place data` | `AdditionalFeatures` `Access`, `Contact`, `Phonemes` or `TimeZone` on a Places command, and the `GeoPlaces` details that ask for them |
331
+
332
+ Everything else is open to every plan: the Standard and Monochrome styles,
333
+ `colorScheme`, `poiDensity`, `poiCategories`, a label language, and the other
334
+ `AdditionalFeatures`. Which plan includes which feature is on the
335
+ [pricing page](https://chaosity.cloud/pricing), and deliberately not here: it can
336
+ change without a release of this package. In the source, every gated option and
337
+ value carries a `@planFeature` tag naming its feature, so your editor shows it.
338
+
339
+ The token carries no feature list, so there is nothing to check beforehand — the
340
+ 403 is how you find out, and it is typed:
341
+
342
+ ```typescript
343
+ import {
344
+ LocationServiceException,
345
+ fetchMapStyle,
346
+ } from '@chaosity/location-client'
347
+
348
+ try {
349
+ map.setStyle(await fetchMapStyle(apiUrl, 'Standard', getToken, options))
350
+ } catch (err) {
351
+ if (err instanceof LocationServiceException && err.isFeatureNotEntitled) {
352
+ showMessage(err.message) // names the refused feature
353
+ } else throw err
354
+ }
355
+ ```
356
+
357
+ `isAuth` is true for it too, as for every 401 and 403 — but so it is for bad
358
+ credentials, an `Origin` the application does not allow, and a route outside the
359
+ plan, which all need different fixes. Branch on `isFeatureNotEntitled`, or on
360
+ `code` against the exported `FEATURE_NOT_ENTITLED`.
361
+
362
+ **What MapLibre fetches for itself is refused outside this package.**
363
+ `fetchMapStyle`, `fetchStaticMap` and the Places commands reject with the typed
364
+ error above. A style URL from `buildMapStyleUrl` handed to `setStyle`, and every
365
+ tile, is fetched by MapLibre, so a refusal there arrives as a map `error` event:
366
+ `error.status` is 403 and `error.body` is a `Blob` holding the same
367
+ `{ message, code }` JSON.
368
+
369
+ ```typescript
370
+ import { FEATURE_NOT_ENTITLED } from '@chaosity/location-client'
371
+
372
+ map.on('error', async ({ error }) => {
373
+ const { status, body } = error as { status?: number; body?: Blob }
374
+ if (status !== 403 || !body) return
375
+ const { code, message } = JSON.parse(await body.text())
376
+ if (code === FEATURE_NOT_ENTITLED) showMessage(message)
377
+ })
378
+ ```
379
+
173
380
  ### MapLibre Geocoder Integration
174
381
 
175
382
  Requires the optional peers: `npm install maplibre-gl @maplibre/maplibre-gl-geocoder`
@@ -177,7 +384,8 @@ Requires the optional peers: `npm install maplibre-gl @maplibre/maplibre-gl-geoc
177
384
  ```typescript
178
385
  import { GeoPlacesClient, GeoPlaces } from '@chaosity/location-client'
179
386
  import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder'
180
- import maplibregl from 'maplibre-gl'
387
+ import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css'
388
+ import * as maplibregl from 'maplibre-gl'
181
389
 
182
390
  // GeoPlaces adapter takes a GeoPlacesClient instance and the map
183
391
  const client = new GeoPlacesClient({ apiUrl, token })
@@ -219,6 +427,22 @@ await client.send(command)
219
427
 
220
428
  When `getToken` is provided, it is called on every request so token updates are reflected without recreating the client.
221
429
 
430
+ `client.getAppConfig()` — and `await connector.getAppConfig()` on the server —
431
+ returns the application's own settings as its access token carries them:
432
+
433
+ ```typescript
434
+ {
435
+ allowedResources?: string[] // e.g. 'GET /maps/static/{fileName}'
436
+ allowedDomain?: string // the domain its requests must come from
437
+ countries?: string[] // ISO 3166-1 alpha-2, once a scope is configured
438
+ }
439
+ ```
440
+
441
+ For display — "static maps are not in your plan", the domain to show beside an
442
+ `Origin not allowed` 403 — never for refusing or shaping a request. The token
443
+ can be up to fifteen minutes old, and the API reads all three fresh from the
444
+ application on every request.
445
+
222
446
  `refreshToken` covers the case `getToken` cannot. `getToken` is synchronous —
223
447
  MapLibre's `transformRequest` requires that — so it can only ever return the
224
448
  token already in hand, and a token the API stops accepting **before** its `exp`
@@ -232,8 +456,8 @@ worth a second round trip. A 403 is never retried: a new token cannot fix an
232
456
  #### Request options
233
457
 
234
458
  Every call in this package takes the same options object — `client.send`,
235
- `connector.send`, `fetchMapStyle` and `fetchStaticMap` — and every failure
236
- arrives as a `LocationServiceException`.
459
+ `connector.send`, `verifyAddress` on either, `fetchMapStyle` and
460
+ `fetchStaticMap` — and every failure arrives as a `LocationServiceException`.
237
461
 
238
462
  ```typescript
239
463
  await client.send(command, {
@@ -245,12 +469,13 @@ await client.send(command, {
245
469
  ```
246
470
 
247
471
  `overallTimeoutMs` is the one worth setting deliberately, and it defaults to
248
- 30 s. `timeoutMs` bounds an attempt, not a call: the API answers a spent quota
249
- with `Retry-After: 60`, and honouring that literally across two retries blocked
250
- the caller for about two minutes — past any Lambda budget. Now no attempt gets
251
- more time than the call has left, and a retry that would have to wait longer
252
- than the remaining budget is not made at all. You get the API's own error back
253
- instead, `retryAfterMs` intact, so you can queue the work rather than guess.
472
+ 30 s. `timeoutMs` bounds an attempt, not a call: nothing bounds the
473
+ `Retry-After` a 429 carries, and honouring one of 60 s literally across two
474
+ retries would have blocked the caller for about two minutes — past any Lambda
475
+ budget. Now no attempt gets more time than the call has left, and a retry that
476
+ would have to wait longer than the remaining budget is not made at all. You get
477
+ the API's own error back instead, `retryAfterMs` intact, so you can queue the
478
+ work rather than guess.
254
479
 
255
480
  The map helpers take them as a trailing argument:
256
481
 
@@ -264,7 +489,7 @@ const style = await fetchMapStyle(
264
489
  )
265
490
  const blob = await fetchStaticMap(
266
491
  apiUrl,
267
- { width: 640, height: 400, center },
492
+ { width: 640, height: 400, center, zoom: 14, style: 'Standard' },
268
493
  getToken,
269
494
  { signal },
270
495
  )
@@ -311,20 +536,29 @@ import { fetchMapStyle } from '@chaosity/location-client'
311
536
 
312
537
  const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
313
538
  colorScheme: 'Dark',
539
+ poiDensity: 'Sparse',
540
+ language: 'fr',
541
+ })
542
+ ```
543
+
544
+ Hand `style` to `new maplibregl.Map`, with the worker set, as in [MapLibre Map Integration](#maplibre-map-integration).
545
+
546
+ The overlays need the `terrain`, `buildings`, `contours`, `traffic` and `travel-modes` plan features — see [Plan features](#plan-features):
547
+
548
+ ```typescript
549
+ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
314
550
  terrain: 'Terrain3D',
315
551
  buildings: 'Buildings3D',
316
552
  contourDensity: 'Medium',
317
553
  traffic: 'All',
318
554
  travelModes: ['Truck', 'Transit'],
319
- language: 'fr',
320
- })
321
-
322
- const map = new maplibregl.Map({
323
- style,
324
- transformRequest: createTransformRequest(apiUrl, getToken),
325
555
  })
326
556
  ```
327
557
 
558
+ This is the call that surfaces a refusal as `LocationServiceException`; a URL
559
+ MapLibre fetches for itself reports it as a map `error` event instead (see
560
+ [Plan features](#plan-features)).
561
+
328
562
  #### buildMapStyleUrl
329
563
 
330
564
  Builds the style descriptor URL without fetching. Useful when you want to pass the URL directly to MapLibre (e.g. without pre-applying language).
@@ -334,23 +568,26 @@ import { buildMapStyleUrl } from '@chaosity/location-client'
334
568
 
335
569
  const url = buildMapStyleUrl(apiUrl, 'Standard', {
336
570
  colorScheme: 'Dark',
337
- terrain: 'Hillshade',
338
571
  })
339
572
  ```
340
573
 
341
574
  #### MapStyleOptions
342
575
 
343
- ```typescript
344
- interface MapStyleOptions {
345
- colorScheme?: 'Light' | 'Dark'
346
- politicalView?: string // ISO 3166-1 alpha-3 (e.g. 'IND', 'TUR')
347
- terrain?: 'Hillshade' | 'Terrain3D'
348
- buildings?: 'Buildings3D'
349
- contourDensity?: 'Medium' // Only 'Medium' is supported by the AWS SDK
350
- traffic?: 'All'
351
- travelModes?: Array<'Truck' | 'Transit'>
352
- }
353
- ```
576
+ The options, the values each accepts, and which of them are plan features are
577
+ documented on the type — `MapStyleOptions` in `src/maps/mapStyle.ts`, with the
578
+ value lists in `src/maps/mapEnums.ts` — and your editor shows them as you type.
579
+ They are not copied here, because the copy that used to be here drifted from the
580
+ source.
581
+
582
+ Every accepted value is exported as an array (`POI_DENSITIES`,
583
+ `STYLE_POI_CATEGORIES`, `TRAFFIC_MODES`, …) so a picker can be built from it. A
584
+ list tagged `@planFeature` — `MAP_STYLES`, `TERRAINS`, `TRAFFIC_MODES` and the
585
+ others in [Plan features](#plan-features) — holds values some plans are refused,
586
+ so leave those out of a picker or mark them. Values are case sensitive, and some
587
+ combinations are the API's to refuse — for example `traffic: 'All'` on
588
+ Satellite. There is no `language` here:
589
+ `fetchMapStyle` takes `language` separately and applies it to the descriptor
590
+ itself, because the service's style descriptor has no language parameter.
354
591
 
355
592
  #### applyMapLanguage
356
593
 
@@ -378,10 +615,12 @@ setAllPoiVisibility(map, false)
378
615
 
379
616
  #### Available Commands
380
617
 
381
- All AWS Location Service commands from `@aws-sdk/client-geo-places`:
618
+ The Amazon Location Places commands, with the SDK's inputs minus the two
619
+ fields below:
382
620
 
383
621
  ```typescript
384
622
  import {
623
+ AutocompleteCommand,
385
624
  SuggestCommand,
386
625
  GeocodeCommand,
387
626
  ReverseGeocodeCommand,
@@ -391,6 +630,29 @@ import {
391
630
  } from '@chaosity/location-client'
392
631
  ```
393
632
 
633
+ **Two SDK fields are not accepted: `IntendedUse` and `Key`.** The service
634
+ removes both from every request — `IntendedUse` would choose the price bucket,
635
+ and `Key` would bill an Amazon Location key that is not the service's — so the
636
+ commands exported here are typed without them, and passing either is a compile
637
+ error rather than a field that is sent and silently ignored. The exported
638
+ `<Name>CommandInput` and `<Name>Request` types are narrowed the same way. At
639
+ runtime nothing is removed: the request body is your input, unchanged, and a
640
+ field cast past the type is stripped by the service all the same.
641
+
642
+ **`AdditionalFeatures` `Access`, `Contact`, `Phonemes` and `TimeZone` are rich
643
+ place data**, a plan feature. On a plan without it the request
644
+ is refused 403 `FeatureNotEntitledException` — see
645
+ [Plan features](#plan-features). `SecondaryAddresses`, `Intersections`,
646
+ `CrossReferences` and `Core` are open to every plan.
647
+
648
+ `VerifyAddressCommand` is this package's own: `POST /address/verify` has no
649
+ SDK command. Its input is `{ PlaceId }` and nothing else, and it answers a
650
+ `VerifyAddressResponse` — see [Verifying an address](#verifying-an-address).
651
+
652
+ ```typescript
653
+ import { VerifyAddressCommand } from '@chaosity/location-client'
654
+ ```
655
+
394
656
  #### Data Type Utilities
395
657
 
396
658
  GeoJSON conversion utilities from `@aws/amazon-location-utilities-datatypes`:
@@ -460,7 +722,11 @@ const connector = new LocationServiceConnector({
460
722
  })
461
723
 
462
724
  const result = await connector.send(
463
- new SuggestCommand({ QueryText: 'Vancouver' }),
725
+ new SuggestCommand({
726
+ QueryText: 'Vancouver',
727
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
728
+ BiasPosition: [-123.1207, 49.2827],
729
+ }),
464
730
  )
465
731
  ```
466
732
 
@@ -533,7 +799,11 @@ Full TypeScript support with types from AWS SDK:
533
799
  import type { SuggestCommandOutput } from '@chaosity/location-client'
534
800
 
535
801
  const response: SuggestCommandOutput = await client.send(
536
- new SuggestCommand({ QueryText: 'Vancouver' }),
802
+ new SuggestCommand({
803
+ QueryText: 'Vancouver',
804
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
805
+ BiasPosition: [-123.1207, 49.2827],
806
+ }),
537
807
  )
538
808
  ```
539
809
 
@@ -25,15 +25,42 @@ import type { GeoPlacesClient } from '../client/GeoPlacesClient.js';
25
25
  * Worth knowing before enabling `contact`: for a street address it costs
26
26
  * Advanced and returns no contact field at all — only points of interest carry
27
27
  * one. On an address-completion flow that is 3x the price for nothing.
28
+ *
29
+ * `access`, `contact` and `timeZone` are also RICH PLACE DATA, a feature of the
30
+ * application's plan (#55). On a plan without it, the lookup is refused 403
31
+ * `FeatureNotEntitledException` — `isFeatureNotEntitled` on the error — and
32
+ * nothing is returned or billed. `secondaryAddresses` is open to every plan.
33
+ * Which plans include it: https://chaosity.cloud/pricing.
28
34
  */
29
35
  export interface GeoPlacesDetailOptions {
30
- /** Entrance/exit points. Moves the request to the Advanced bucket. */
36
+ /**
37
+ * Entrance/exit points. Moves the request to the Advanced bucket.
38
+ *
39
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
40
+ * `FEATURE_NOT_ENTITLED`).
41
+ *
42
+ * @planFeature rich place data
43
+ */
31
44
  access?: boolean;
32
45
  /** Unit and sub-address detail. Stays in the Core bucket — free to enable. */
33
46
  secondaryAddresses?: boolean;
34
- /** Phone/website, POIs only. Moves the request to the Advanced bucket. */
47
+ /**
48
+ * Phone/website, POIs only. Moves the request to the Advanced bucket.
49
+ *
50
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
51
+ * `FEATURE_NOT_ENTITLED`).
52
+ *
53
+ * @planFeature rich place data
54
+ */
35
55
  contact?: boolean;
36
- /** IANA zone and offset. Moves the request to the Advanced bucket. */
56
+ /**
57
+ * IANA zone and offset. Moves the request to the Advanced bucket.
58
+ *
59
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
60
+ * `FEATURE_NOT_ENTITLED`).
61
+ *
62
+ * @planFeature rich place data
63
+ */
37
64
  timeZone?: boolean;
38
65
  }
39
66
  export interface GeoPlacesOptions {
@@ -1,6 +1,10 @@
1
- import { GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeocodeCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
1
+ import { GetPlaceAdditionalFeature, } from '@aws-sdk/client-geo-places';
2
2
  import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
3
3
  import debug from 'debug';
4
+ import {
5
+ // The package's own narrowed commands, not the SDK's: an input typed with
6
+ // the SDK's would admit IntendedUse and Key, which the service strips (#40).
7
+ GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SuggestCommand, } from '../client/commands.js';
4
8
  const log = debug('location-client:geocoder');
5
9
  /**
6
10
  * Give the control the fields it renders, not just the ones GeoJSON needs.
@@ -25,15 +25,42 @@ import type { GeoPlacesClient } from '../client/GeoPlacesClient.js';
25
25
  * Worth knowing before enabling `contact`: for a street address it costs
26
26
  * Advanced and returns no contact field at all — only points of interest carry
27
27
  * one. On an address-completion flow that is 3x the price for nothing.
28
+ *
29
+ * `access`, `contact` and `timeZone` are also RICH PLACE DATA, a feature of the
30
+ * application's plan (#55). On a plan without it, the lookup is refused 403
31
+ * `FeatureNotEntitledException` — `isFeatureNotEntitled` on the error — and
32
+ * nothing is returned or billed. `secondaryAddresses` is open to every plan.
33
+ * Which plans include it: https://chaosity.cloud/pricing.
28
34
  */
29
35
  export interface GeoPlacesDetailOptions {
30
- /** Entrance/exit points. Moves the request to the Advanced bucket. */
36
+ /**
37
+ * Entrance/exit points. Moves the request to the Advanced bucket.
38
+ *
39
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
40
+ * `FEATURE_NOT_ENTITLED`).
41
+ *
42
+ * @planFeature rich place data
43
+ */
31
44
  access?: boolean;
32
45
  /** Unit and sub-address detail. Stays in the Core bucket — free to enable. */
33
46
  secondaryAddresses?: boolean;
34
- /** Phone/website, POIs only. Moves the request to the Advanced bucket. */
47
+ /**
48
+ * Phone/website, POIs only. Moves the request to the Advanced bucket.
49
+ *
50
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
51
+ * `FEATURE_NOT_ENTITLED`).
52
+ *
53
+ * @planFeature rich place data
54
+ */
35
55
  contact?: boolean;
36
- /** IANA zone and offset. Moves the request to the Advanced bucket. */
56
+ /**
57
+ * IANA zone and offset. Moves the request to the Advanced bucket.
58
+ *
59
+ * Refused on a plan without it: 403 `FeatureNotEntitledException` (see
60
+ * `FEATURE_NOT_ENTITLED`).
61
+ *
62
+ * @planFeature rich place data
63
+ */
37
64
  timeZone?: boolean;
38
65
  }
39
66
  export interface GeoPlacesOptions {
@@ -7,6 +7,7 @@ exports.GeoPlaces = void 0;
7
7
  const client_geo_places_1 = require("@aws-sdk/client-geo-places");
8
8
  const amazon_location_utilities_datatypes_1 = require("@aws/amazon-location-utilities-datatypes");
9
9
  const debug_1 = __importDefault(require("debug"));
10
+ const commands_js_1 = require("../client/commands.js");
10
11
  const log = (0, debug_1.default)('location-client:geocoder');
11
12
  /**
12
13
  * Give the control the fields it renders, not just the ones GeoJSON needs.
@@ -99,7 +100,7 @@ class GeoPlaces {
99
100
  : config.countries.split(','),
100
101
  };
101
102
  }
102
- const response = (await this.client.send(new client_geo_places_1.GeocodeCommand(commandInput)));
103
+ const response = (await this.client.send(new commands_js_1.GeocodeCommand(commandInput)));
103
104
  const converted = (0, amazon_location_utilities_datatypes_1.geocodeResponseToFeatureCollection)(response, {
104
105
  flattenProperties: true,
105
106
  });
@@ -120,7 +121,7 @@ class GeoPlaces {
120
121
  MaxResults: config.limit || 1,
121
122
  Language: this.normalizeLanguage(config.language),
122
123
  };
123
- const response = (await this.client.send(new client_geo_places_1.ReverseGeocodeCommand(commandInput)));
124
+ const response = (await this.client.send(new commands_js_1.ReverseGeocodeCommand(commandInput)));
124
125
  const converted = (0, amazon_location_utilities_datatypes_1.reverseGeocodeResponseToFeatureCollection)(response, {
125
126
  flattenProperties: true,
126
127
  });
@@ -169,7 +170,7 @@ class GeoPlaces {
169
170
  }
170
171
  : {}),
171
172
  };
172
- const response = (await this.client.send(new client_geo_places_1.SuggestCommand(commandInput)));
173
+ const response = (await this.client.send(new commands_js_1.SuggestCommand(commandInput)));
173
174
  const suggestions = { suggestions: [] };
174
175
  for (const item of response.ResultItems ?? []) {
175
176
  const text = item.Title;
@@ -187,7 +188,7 @@ class GeoPlaces {
187
188
  // lookup in the Advanced bucket at $1.50/1k; the default now sends none
188
189
  // and stays in Core at $0.50. Callers that want the detail ask for it.
189
190
  const additionalFeatures = this.detailFeatures();
190
- const command = new client_geo_places_1.GetPlaceCommand({
191
+ const command = new commands_js_1.GetPlaceCommand({
191
192
  PlaceId: config.query,
192
193
  Language: this.normalizeLanguage(config.language),
193
194
  ...(additionalFeatures ? { AdditionalFeatures: additionalFeatures } : {}),