@chaosity/location-client 0.8.0 → 0.10.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 (54) hide show
  1. package/README.md +264 -42
  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 +24 -10
  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 +32 -14
  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 +37 -13
  27. package/dist/cjs/utils/tokenClaims.js +36 -14
  28. package/dist/client/GeoPlacesClient.d.ts +16 -4
  29. package/dist/client/GeoPlacesClient.js +24 -10
  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 +32 -14
  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 +37 -13
  49. package/dist/utils/tokenClaims.js +36 -14
  50. package/package.json +1 -1
  51. package/dist/cjs/utils/roundPosition.d.ts +0 -66
  52. package/dist/cjs/utils/roundPosition.js +0 -109
  53. package/dist/utils/roundPosition.d.ts +0 -66
  54. package/dist/utils/roundPosition.js +0 -104
package/README.md CHANGED
@@ -38,9 +38,10 @@ npm install maplibre-gl @maplibre/maplibre-gl-geocoder
38
38
 
39
39
  - **Custom Authentication**: Uses Bearer tokens instead of AWS SigV4
40
40
  - **AWS SDK Commands**: Full access to all AWS Location Service commands
41
+ - **Address Verification**: `verifyAddress(placeId)` returns the one Places result you may store — see [Verifying an address](#verifying-an-address)
41
42
  - **Data Type Utilities**: Built-in GeoJSON conversion utilities
42
43
  - **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
44
+ - **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
45
  - **Map Language**: Switch map label language client-side with zero API calls
45
46
  - **POI Layer Control**: Toggle point-of-interest categories on/off by layer
46
47
  - **Server Utilities**: `getClientConfig()` with auto-env detection and token caching
@@ -101,10 +102,78 @@ const client = new GeoPlacesClient({
101
102
  })
102
103
 
103
104
  const response: SuggestCommandOutput = await client.send(
104
- new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 }),
105
+ new SuggestCommand({
106
+ QueryText: 'Vancouver',
107
+ MaxResults: 5,
108
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
109
+ BiasPosition: [-123.1207, 49.2827],
110
+ }),
105
111
  )
106
112
  ```
107
113
 
114
+ ### Verifying an address
115
+
116
+ `POST /address/verify` resolves one PlaceId to the full place record plus
117
+ `verified`. The PlaceId can come from any suggestion, autocomplete, geocode or
118
+ place result, a unit's included. The answer is **the one Places result you may
119
+ store**; every other result is for display only. The exception is a place in
120
+ Japan, which may not be stored at all.
121
+
122
+ The flow is suggest, then an optional unit pick, then verify at submit:
123
+
124
+ ```typescript
125
+ import {
126
+ GetPlaceCommand,
127
+ SuggestCommand,
128
+ type GetPlaceCommandOutput,
129
+ type SuggestCommandOutput,
130
+ } from '@chaosity/location-client'
131
+
132
+ // 1. While the person types: suggestions, for display.
133
+ const suggestions: SuggestCommandOutput = await client.send(
134
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
135
+ new SuggestCommand({
136
+ QueryText: '100 George St, Sydney',
137
+ BiasPosition: [151.2093, -33.8688],
138
+ }),
139
+ )
140
+ const picked = suggestions.ResultItems?.[0]?.Place?.PlaceId
141
+
142
+ // 2. Optionally, offer the building's units. Each has its own PlaceId.
143
+ const building: GetPlaceCommandOutput = await client.send(
144
+ new GetPlaceCommand({
145
+ PlaceId: picked!,
146
+ AdditionalFeatures: ['SecondaryAddresses'],
147
+ }),
148
+ )
149
+ const unit = building.SecondaryAddresses?.[0]?.PlaceId
150
+
151
+ // 3. At submit, once: verify the PlaceId that was chosen.
152
+ const answer = await client.verifyAddress(unit ?? picked!)
153
+ if (answer.verified) {
154
+ // `answer` is the record you may keep.
155
+ }
156
+ ```
157
+
158
+ - `verified` is `true` for a `PointAddress`, or a `SecondaryAddress` (a unit).
159
+ It is `false` for anything else: an interpolated address, a street, a
160
+ locality, a point of interest. A `false` is still a 200, so the call
161
+ resolves; it does not throw.
162
+ - **Every verify is billed, whether or not the address verifies.** Call it
163
+ once per chosen PlaceId, at submit. Never call it per keystroke, or on every
164
+ pick.
165
+ - Only the `PlaceId` is sent. `VerifyAddressCommandInput` has no other field,
166
+ because the service would drop `Language`, `PoliticalView` and
167
+ `AdditionalFeatures`. A repeat verify of the same PlaceId may be answered
168
+ from the service's own store, and is billed all the same.
169
+ - Keep the PlaceId you sent beside the answer. The answer's own `PlaceId` can
170
+ differ, and for a unit it does. The service does not accept that one back,
171
+ while the one you sent verifies again.
172
+ - `client.verifyAddress(placeId)` is
173
+ `client.send(new VerifyAddressCommand({ PlaceId: placeId }))`, typed as
174
+ `VerifyAddressResponse`. `connector.verifyAddress` does the same on the
175
+ server.
176
+
108
177
  ### MapLibre Map Integration
109
178
 
110
179
  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:
@@ -118,8 +187,6 @@ import maplibregl from 'maplibre-gl'
118
187
 
119
188
  const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
120
189
  colorScheme: 'Dark',
121
- terrain: 'Terrain3D',
122
- buildings: 'Buildings3D',
123
190
  language: 'fr',
124
191
  })
125
192
 
@@ -128,11 +195,20 @@ const map = new maplibregl.Map({
128
195
  style,
129
196
  center: [-123.12, 49.28],
130
197
  zoom: 10,
131
- maxPitch: 85,
132
198
  transformRequest: createTransformRequest(apiUrl, getToken),
133
199
  })
134
200
  ```
135
201
 
202
+ 3D terrain and buildings need the `terrain` and `buildings` plan features — see [Plan features](#plan-features):
203
+
204
+ ```typescript
205
+ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
206
+ terrain: 'Terrain3D',
207
+ buildings: 'Buildings3D',
208
+ })
209
+ // then `maxPitch: 85` on the map, so the camera can tilt to see them
210
+ ```
211
+
136
212
  ### Switching Map Language
137
213
 
138
214
  Change map label language instantly on the client side — no API calls needed:
@@ -170,6 +246,74 @@ setAllPoiVisibility(map, false)
170
246
 
171
247
  Available categories: `food_drink`, `entertainment`, `sights`, `transit`, `accommodations`, `leisure`, `shopping`, `business`, `facilities`, `areas`, `parks`.
172
248
 
249
+ ### Plan features
250
+
251
+ Some options are features of the application's plan. An application whose plan
252
+ does not include one is refused **403 `FeatureNotEntitledException`** before
253
+ anything is fetched upstream, so the refusal is not billed, and the message
254
+ names each refused feature and the option that asked for it:
255
+
256
+ > This application's plan does not include the map feature terrain (terrain=Terrain3D).
257
+
258
+ | Feature | What asks for it |
259
+ | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
260
+ | `satellite` | the `Satellite` and `Hybrid` styles, and a static map with no `style` — Satellite is its default |
261
+ | `terrain` | `terrain` (`Hillshade`, `Terrain3D`) |
262
+ | `buildings` | `buildings` |
263
+ | `contours` | `contourDensity` |
264
+ | `traffic` | `traffic` |
265
+ | `travel-modes` | `travelModes` |
266
+ | `political-view` | `politicalView`, on a style or a static map |
267
+ | `rich place data` | `AdditionalFeatures` `Access`, `Contact`, `Phonemes` or `TimeZone` on a Places command, and the `GeoPlaces` details that ask for them |
268
+
269
+ Everything else is open to every plan: the Standard and Monochrome styles,
270
+ `colorScheme`, `poiDensity`, `poiCategories`, a label language, and the other
271
+ `AdditionalFeatures`. Which plan includes which feature is on the
272
+ [pricing page](https://chaosity.cloud/pricing), and deliberately not here: it can
273
+ change without a release of this package. In the source, every gated option and
274
+ value carries a `@planFeature` tag naming its feature, so your editor shows it.
275
+
276
+ The token carries no feature list, so there is nothing to check beforehand — the
277
+ 403 is how you find out, and it is typed:
278
+
279
+ ```typescript
280
+ import {
281
+ LocationServiceException,
282
+ fetchMapStyle,
283
+ } from '@chaosity/location-client'
284
+
285
+ try {
286
+ map.setStyle(await fetchMapStyle(apiUrl, 'Standard', getToken, options))
287
+ } catch (err) {
288
+ if (err instanceof LocationServiceException && err.isFeatureNotEntitled) {
289
+ showMessage(err.message) // names the refused feature
290
+ } else throw err
291
+ }
292
+ ```
293
+
294
+ `isAuth` is true for it too, as for every 401 and 403 — but so it is for bad
295
+ credentials, an `Origin` the application does not allow, and a route outside the
296
+ plan, which all need different fixes. Branch on `isFeatureNotEntitled`, or on
297
+ `code` against the exported `FEATURE_NOT_ENTITLED`.
298
+
299
+ **What MapLibre fetches for itself is refused outside this package.**
300
+ `fetchMapStyle`, `fetchStaticMap` and the Places commands reject with the typed
301
+ error above. A style URL from `buildMapStyleUrl` handed to `setStyle`, and every
302
+ tile, is fetched by MapLibre, so a refusal there arrives as a map `error` event:
303
+ `error.status` is 403 and `error.body` is a `Blob` holding the same
304
+ `{ message, code }` JSON.
305
+
306
+ ```typescript
307
+ import { FEATURE_NOT_ENTITLED } from '@chaosity/location-client'
308
+
309
+ map.on('error', async ({ error }) => {
310
+ const { status, body } = error as { status?: number; body?: Blob }
311
+ if (status !== 403 || !body) return
312
+ const { code, message } = JSON.parse(await body.text())
313
+ if (code === FEATURE_NOT_ENTITLED) showMessage(message)
314
+ })
315
+ ```
316
+
173
317
  ### MapLibre Geocoder Integration
174
318
 
175
319
  Requires the optional peers: `npm install maplibre-gl @maplibre/maplibre-gl-geocoder`
@@ -219,6 +363,22 @@ await client.send(command)
219
363
 
220
364
  When `getToken` is provided, it is called on every request so token updates are reflected without recreating the client.
221
365
 
366
+ `client.getAppConfig()` — and `await connector.getAppConfig()` on the server —
367
+ returns the application's own settings as its access token carries them:
368
+
369
+ ```typescript
370
+ {
371
+ allowedResources?: string[] // e.g. 'GET /maps/static/{fileName}'
372
+ allowedDomain?: string // the domain its requests must come from
373
+ countries?: string[] // ISO 3166-1 alpha-2, once a scope is configured
374
+ }
375
+ ```
376
+
377
+ For display — "static maps are not in your plan", the domain to show beside an
378
+ `Origin not allowed` 403 — never for refusing or shaping a request. The token
379
+ can be up to fifteen minutes old, and the API reads all three fresh from the
380
+ application on every request.
381
+
222
382
  `refreshToken` covers the case `getToken` cannot. `getToken` is synchronous —
223
383
  MapLibre's `transformRequest` requires that — so it can only ever return the
224
384
  token already in hand, and a token the API stops accepting **before** its `exp`
@@ -232,8 +392,8 @@ worth a second round trip. A 403 is never retried: a new token cannot fix an
232
392
  #### Request options
233
393
 
234
394
  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`.
395
+ `connector.send`, `verifyAddress` on either, `fetchMapStyle` and
396
+ `fetchStaticMap` — and every failure arrives as a `LocationServiceException`.
237
397
 
238
398
  ```typescript
239
399
  await client.send(command, {
@@ -245,12 +405,13 @@ await client.send(command, {
245
405
  ```
246
406
 
247
407
  `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.
408
+ 30 s. `timeoutMs` bounds an attempt, not a call: nothing bounds the
409
+ `Retry-After` a 429 carries, and honouring one of 60 s literally across two
410
+ retries would have blocked the caller for about two minutes — past any Lambda
411
+ budget. Now no attempt gets more time than the call has left, and a retry that
412
+ would have to wait longer than the remaining budget is not made at all. You get
413
+ the API's own error back instead, `retryAfterMs` intact, so you can queue the
414
+ work rather than guess.
254
415
 
255
416
  The map helpers take them as a trailing argument:
256
417
 
@@ -264,7 +425,7 @@ const style = await fetchMapStyle(
264
425
  )
265
426
  const blob = await fetchStaticMap(
266
427
  apiUrl,
267
- { width: 640, height: 400, center },
428
+ { width: 640, height: 400, center, zoom: 14, style: 'Standard' },
268
429
  getToken,
269
430
  { signal },
270
431
  )
@@ -311,11 +472,7 @@ import { fetchMapStyle } from '@chaosity/location-client'
311
472
 
312
473
  const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
313
474
  colorScheme: 'Dark',
314
- terrain: 'Terrain3D',
315
- buildings: 'Buildings3D',
316
- contourDensity: 'Medium',
317
- traffic: 'All',
318
- travelModes: ['Truck', 'Transit'],
475
+ poiDensity: 'Sparse',
319
476
  language: 'fr',
320
477
  })
321
478
 
@@ -325,6 +482,22 @@ const map = new maplibregl.Map({
325
482
  })
326
483
  ```
327
484
 
485
+ The overlays need the `terrain`, `buildings`, `contours`, `traffic` and `travel-modes` plan features — see [Plan features](#plan-features):
486
+
487
+ ```typescript
488
+ const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
489
+ terrain: 'Terrain3D',
490
+ buildings: 'Buildings3D',
491
+ contourDensity: 'Medium',
492
+ traffic: 'All',
493
+ travelModes: ['Truck', 'Transit'],
494
+ })
495
+ ```
496
+
497
+ This is the call that surfaces a refusal as `LocationServiceException`; a URL
498
+ MapLibre fetches for itself reports it as a map `error` event instead (see
499
+ [Plan features](#plan-features)).
500
+
328
501
  #### buildMapStyleUrl
329
502
 
330
503
  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 +507,26 @@ import { buildMapStyleUrl } from '@chaosity/location-client'
334
507
 
335
508
  const url = buildMapStyleUrl(apiUrl, 'Standard', {
336
509
  colorScheme: 'Dark',
337
- terrain: 'Hillshade',
338
510
  })
339
511
  ```
340
512
 
341
513
  #### MapStyleOptions
342
514
 
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
- ```
515
+ The options, the values each accepts, and which of them are plan features are
516
+ documented on the type — `MapStyleOptions` in `src/maps/mapStyle.ts`, with the
517
+ value lists in `src/maps/mapEnums.ts` — and your editor shows them as you type.
518
+ They are not copied here, because the copy that used to be here drifted from the
519
+ source.
520
+
521
+ Every accepted value is exported as an array (`POI_DENSITIES`,
522
+ `STYLE_POI_CATEGORIES`, `TRAFFIC_MODES`, …) so a picker can be built from it. A
523
+ list tagged `@planFeature` — `MAP_STYLES`, `TERRAINS`, `TRAFFIC_MODES` and the
524
+ others in [Plan features](#plan-features) — holds values some plans are refused,
525
+ so leave those out of a picker or mark them. Values are case sensitive, and some
526
+ combinations are the API's to refuse — for example `traffic: 'All'` on
527
+ Satellite. There is no `language` here:
528
+ `fetchMapStyle` takes `language` separately and applies it to the descriptor
529
+ itself, because the service's style descriptor has no language parameter.
354
530
 
355
531
  #### applyMapLanguage
356
532
 
@@ -378,10 +554,12 @@ setAllPoiVisibility(map, false)
378
554
 
379
555
  #### Available Commands
380
556
 
381
- All AWS Location Service commands from `@aws-sdk/client-geo-places`:
557
+ The Amazon Location Places commands, with the SDK's inputs minus the two
558
+ fields below:
382
559
 
383
560
  ```typescript
384
561
  import {
562
+ AutocompleteCommand,
385
563
  SuggestCommand,
386
564
  GeocodeCommand,
387
565
  ReverseGeocodeCommand,
@@ -391,6 +569,29 @@ import {
391
569
  } from '@chaosity/location-client'
392
570
  ```
393
571
 
572
+ **Two SDK fields are not accepted: `IntendedUse` and `Key`.** The service
573
+ removes both from every request — `IntendedUse` would choose the price bucket,
574
+ and `Key` would bill an Amazon Location key that is not the service's — so the
575
+ commands exported here are typed without them, and passing either is a compile
576
+ error rather than a field that is sent and silently ignored. The exported
577
+ `<Name>CommandInput` and `<Name>Request` types are narrowed the same way. At
578
+ runtime nothing is removed: the request body is your input, unchanged, and a
579
+ field cast past the type is stripped by the service all the same.
580
+
581
+ **`AdditionalFeatures` `Access`, `Contact`, `Phonemes` and `TimeZone` are rich
582
+ place data**, a plan feature. On a plan without it the request
583
+ is refused 403 `FeatureNotEntitledException` — see
584
+ [Plan features](#plan-features). `SecondaryAddresses`, `Intersections`,
585
+ `CrossReferences` and `Core` are open to every plan.
586
+
587
+ `VerifyAddressCommand` is this package's own: `POST /address/verify` has no
588
+ SDK command. Its input is `{ PlaceId }` and nothing else, and it answers a
589
+ `VerifyAddressResponse` — see [Verifying an address](#verifying-an-address).
590
+
591
+ ```typescript
592
+ import { VerifyAddressCommand } from '@chaosity/location-client'
593
+ ```
594
+
394
595
  #### Data Type Utilities
395
596
 
396
597
  GeoJSON conversion utilities from `@aws/amazon-location-utilities-datatypes`:
@@ -460,7 +661,11 @@ const connector = new LocationServiceConnector({
460
661
  })
461
662
 
462
663
  const result = await connector.send(
463
- new SuggestCommand({ QueryText: 'Vancouver' }),
664
+ new SuggestCommand({
665
+ QueryText: 'Vancouver',
666
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
667
+ BiasPosition: [-123.1207, 49.2827],
668
+ }),
464
669
  )
465
670
  ```
466
671
 
@@ -485,17 +690,30 @@ const connector = new LocationServiceConnector({
485
690
  })
486
691
  ```
487
692
 
488
- ## Cache-Friendly Position Rounding
693
+ ## Coordinates Are Sent As You Supply Them
694
+
695
+ Both `GeoPlacesClient` and `LocationServiceConnector` put your command input on
696
+ the wire unchanged. `BiasPosition` and `QueryPosition` arrive at the service at
697
+ the precision you passed.
489
698
 
490
- `BiasPosition` coordinates are automatically rounded before each API request, to
491
- whatever precision your application is entitled to — a `biasDecimals` claim on
492
- the access token, defaulting to **3 decimal places** (~110 m) when the token
493
- carries none. This maximizes cache hits across nearby users without affecting
494
- result quality — bias is approximate by nature.
699
+ Earlier versions rounded `BiasPosition` to a grid — 3 decimal places by
700
+ default — so that nearby callers could share a cached upstream answer. Requests
701
+ are no longer cached, so the rounding had nothing left to share and only
702
+ lowered the precision the geocoder worked from.
495
703
 
496
- `QueryPosition` (reverse geocode) retains full precision since it represents an exact point the user selected.
704
+ That is not a coarser result, it is a different one. A 3 dp grid moves a
705
+ coordinate by up to ~70 m, depending where in its cell the coordinate falls,
706
+ and the places a search returns change well inside that distance: measured
707
+ against this service, a bias moved ~70 m returned a different set of nearby
708
+ places, not the same set in a different order. If you were relying on the
709
+ rounding to group nearby requests, round before you call.
497
710
 
498
- This is handled transparently in both `GeoPlacesClient` and `LocationServiceConnector` — no action needed in application code.
711
+ One coordinate is normalised: a static map's `center`, `bounding-box` and
712
+ `bounded-positions` are rounded to six decimals — ~10 cm, below one pixel of a
713
+ raster render. That is this library's choice, well inside what the service
714
+ accepts: at most fourteen decimals per number, and at most 36 characters for
715
+ the pair. A value straight from `map.getCenter()` carries fifteen or sixteen
716
+ decimals and fails the first of those. A format rule, not a precision policy.
499
717
 
500
718
  ## Logging
501
719
 
@@ -520,7 +738,11 @@ Full TypeScript support with types from AWS SDK:
520
738
  import type { SuggestCommandOutput } from '@chaosity/location-client'
521
739
 
522
740
  const response: SuggestCommandOutput = await client.send(
523
- new SuggestCommand({ QueryText: 'Vancouver' }),
741
+ new SuggestCommand({
742
+ QueryText: 'Vancouver',
743
+ // Suggest takes exactly one of BiasPosition, Filter.BoundingBox or Filter.Circle.
744
+ BiasPosition: [-123.1207, 49.2827],
745
+ }),
524
746
  )
525
747
  ```
526
748
 
@@ -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 } : {}),
@@ -1,6 +1,7 @@
1
1
  import type { RequestOptions } from '../transport/http.js';
2
2
  import type { ClientConfig } from '../types/index.js';
3
3
  import type { AppConfigClaims } from '../utils/tokenClaims.js';
4
+ import type { VerifyAddressResponse } from './commands.js';
4
5
  export type SendOptions = RequestOptions;
5
6
  /**
6
7
  * GeoPlacesClient — AWS Location Service compatible client with custom auth.
@@ -18,17 +19,19 @@ export declare class GeoPlacesClient {
18
19
  constructor(config: ClientConfig);
19
20
  /**
20
21
  * This application's own configuration, as carried on the access token
21
- * (api#65) — bias precision, and the countries it is scoped to.
22
+ * (api#65): the routes it may call, the domain its requests must come from,
23
+ * and the countries it is scoped to (#40).
22
24
  *
23
25
  * Provided so an application can SHOW its own settings: populate a country
24
26
  * selector with the markets it actually serves, label a settings screen,
25
27
  * and so on. Being a few minutes stale is cosmetic for that.
26
28
  *
27
29
  * It is not an entitlement check. See AppConfigClaims for why acting on
28
- * `countries` client-side makes requests fail that would otherwise succeed.
30
+ * any of it client-side makes requests fail that would otherwise succeed.
29
31
  *
30
- * Returns `{}` when the token carries no application config, which is the
31
- * case until one is configured in the portal.
32
+ * Every token the API issues carries `allowedResources` and `allowedDomain`;
33
+ * `countries` only once a scope is configured in the portal. Returns `{}`
34
+ * for a token carrying none of them.
32
35
  */
33
36
  getAppConfig(): AppConfigClaims;
34
37
  /** Prefer the getToken callback (live ref) over a static token string. */
@@ -46,5 +49,14 @@ export declare class GeoPlacesClient {
46
49
  * retry loop. Every failure throws LocationServiceException.
47
50
  */
48
51
  send<TInput, TOutput>(command: TInput, options?: SendOptions): Promise<TOutput>;
52
+ /**
53
+ * Verify a PlaceId: `send(new VerifyAddressCommand({ PlaceId }))`, typed
54
+ * (#54). Resolves the full place record plus `verified`, and resolves a
55
+ * `verified: false` too — see VerifyAddressResponse for what may be stored.
56
+ *
57
+ * Billed per call, whether or not the address verifies: call it once per
58
+ * chosen PlaceId, at submit, never per keystroke.
59
+ */
60
+ verifyAddress(placeId: string, options?: SendOptions): Promise<VerifyAddressResponse>;
49
61
  private dispatch;
50
62
  }