@tileflow/search 0.1.0-alpha.2 → 0.1.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  # @tileflow/search
2
2
 
3
- Forward and reverse geocoding, location suggestions, and selected-suggestion resolution. This is a
4
- headless client: it provides no search box, map control, address validation, category discovery, or
5
- batch interface.
3
+ Headless forward/reverse geocoding, autocomplete, category discovery, Nearby POIs and explicit
4
+ selected-place resolution. It uses `fetch` and has no renderer or global interaction state.
6
5
 
7
6
  > Related packages and guides: [documentation index](https://raw.githubusercontent.com/tileflow/tileflow-sdk/main/llms.txt).
8
7
 
@@ -12,134 +11,251 @@ batch interface.
12
11
  npm install @tileflow/search@alpha
13
12
  ```
14
13
 
15
- Use a runtime with `fetch`, such as Node.js 22 or newer. Live requests need an enabled Tileflow
16
- Geocoding service and a Team key or signed Team capability with `geocoding:read`. Installing the
17
- package does not enable that service. Keep privileged credentials on your server; a browser UI
18
- should call your own authorized endpoint.
14
+ Use Node.js 22 or another runtime with `fetch`. Live requests require enabled Tileflow Search and a
15
+ Team credential with `geocoding:read`. Keep privileged credentials on your application server.
16
+ Browser and mobile interfaces call their own authorized server endpoint.
19
17
 
20
- ## Find a place
18
+ Managed operations require paid Starter or a separate, finite Search evaluation grant. A Starter
19
+ trial or complimentary assignment alone does not authorize Search. Free Teams can read categories.
20
+ Installing this package does not enable the service or grant access.
21
21
 
22
- Run this in a trusted server process with `TILEFLOW_API_KEY` set securely:
22
+ ## Find nearby places
23
+
24
+ Run this example on your server with `TILEFLOW_API_KEY` set securely. It lists the current categories,
25
+ then makes one explicit Nearby request using a discovered ID:
23
26
 
24
27
  <!-- docs:check -->
25
28
 
26
29
  ```ts
27
- import {GeocodingError, geocode} from '@tileflow/search';
30
+ import {listCategories, searchNearby} from '@tileflow/search';
28
31
 
29
32
  const apiKey = process.env.TILEFLOW_API_KEY;
30
33
  if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
34
+ const options = {apiKey};
35
+ const catalog = await listCategories(options);
36
+ const category = catalog.categories.find(({name}) => name.toLowerCase().includes('coffee'));
37
+ if (!category) throw new Error('Choose a category from the current catalog.');
31
38
 
32
- try {
33
- const response = await geocode(
34
- {query: 'Praça do Comércio, Lisboa', language: 'en', retention: 'temporary'},
35
- {apiKey, signal: AbortSignal.timeout(5000)},
36
- );
37
- console.log(response.results);
38
- console.log(response.source, response.attribution);
39
- } catch (error) {
40
- if (!(error instanceof GeocodingError)) throw error;
41
- console.error(error.code, error.status, error.requestId);
42
- }
39
+ const page = await searchNearby(
40
+ {
41
+ position: [-9.1393, 38.7223],
42
+ radiusMeters: 1000,
43
+ includeCategories: [category.id],
44
+ countries: ['PT'],
45
+ language: 'en',
46
+ limit: 20,
47
+ },
48
+ options,
49
+ );
50
+
51
+ console.log(page.results, page.attribution, page.usage.units);
43
52
  ```
44
53
 
45
- A successful response may have no results. Present all returned attribution entries alongside
46
- results, preserving any supplied URLs. A source revision of `null` means no reliable revision is
47
- available, not that provenance can be invented or omitted.
54
+ Coordinates are WGS84 `[longitude, latitude]`. Results preserve native AWS proximity ordering.
55
+ A POI's optional `name` is its establishment title; `label` and `address` describe its location.
56
+ Categories and business-chain labels appear only when supplied. Missing metadata does not establish
57
+ opening status, freshness or completeness. Co-located businesses are not merged.
58
+
59
+ Display every attribution entry with results, preserving supplied URLs. Results and opaque references
60
+ are temporary. Do not build a permanent place catalog, cache results, or persist provider references.
48
61
 
49
- ## Reverse-geocode a position
62
+ ## Request another page or resolve a selection
50
63
 
51
- Positions use WGS84 `[longitude, latitude]`, not `[latitude, longitude]`:
64
+ Save the original request in the current interaction. Another page repeats that request with the
65
+ returned `nextCursor`; no page is fetched automatically. A successful empty page can have a cursor.
66
+ Each explicit page costs 25 API units. There is no total-count or snapshot-consistency promise.
52
67
 
53
68
  <!-- docs:check -->
54
69
 
55
70
  ```ts
56
- import {geocodeReverse} from '@tileflow/search';
71
+ import {type NearbyRequest, resolvePlace, searchNearby} from '@tileflow/search';
57
72
 
58
73
  const apiKey = process.env.TILEFLOW_API_KEY;
59
74
  if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
75
+ const options = {apiKey};
76
+ const original: NearbyRequest = {position: [-9.1393, 38.7223], radiusMeters: 1000};
60
77
 
61
- const response = await geocodeReverse(
62
- {
63
- position: [-9.1393, 38.7223],
64
- kinds: ['address', 'place'],
65
- language: 'en',
66
- retention: 'temporary',
67
- },
68
- {apiKey},
69
- );
78
+ export function requestPage(cursor?: string) {
79
+ return searchNearby({...original, ...(cursor ? {cursor} : {})}, options);
80
+ }
70
81
 
71
- console.log(response.results, response.attribution);
82
+ export function resolveSelectedPlace(token: string) {
83
+ return resolvePlace({token}, options);
84
+ }
72
85
  ```
73
86
 
74
- Results are nearby candidates in provider order. They do not establish containment, administrative
75
- membership, parcel or entrance precision, routability, deliverability, or address validity.
76
- `kinds` is an OR filter with 1–4 values from `address`, `street`, `locality`, and `place`; ordering
77
- adds no priority. Reverse `limit` defaults to 1 and accepts 1–10.
87
+ Invoke these handlers only after explicit user actions. Use a returned POI token for resolution;
88
+ map feature IDs, click coordinates and raw AWS Place IDs are not selection tokens. A result without
89
+ a token can be displayed but cannot be resolved. Selection and pagination references cannot be
90
+ interchanged or moved between Teams. Changed query, language, filters or limit invalidates a cursor.
78
91
 
79
- ## Resolve a user-selected suggestion
92
+ `resolvePlace` and `resolveSuggestion` perform the same single resolution operation. Neither fetches
93
+ other results or retries automatically. An expired reference requires an explicit fresh search.
80
94
 
81
- The server-side example returns suggestions first and exposes a separate selection handler:
95
+ ## Geocode text, reverse a position, or offer suggestions
82
96
 
83
97
  <!-- docs:check -->
84
98
 
85
99
  ```ts
86
- import {autocomplete, resolveSuggestion} from '@tileflow/search';
100
+ import {autocomplete, geocode, geocodeReverse, resolveSuggestion} from '@tileflow/search';
87
101
 
88
102
  const apiKey = process.env.TILEFLOW_API_KEY;
89
103
  if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
90
104
  const options = {apiKey};
91
105
 
92
- const response = await autocomplete({query: 'Hospital La Paz', language: 'en'}, options);
93
- console.log(response.suggestions, response.attribution);
94
-
95
- export async function resolveUserSelection(token: string) {
96
- const resolved = await resolveSuggestion({token, retention: 'temporary'}, options);
97
- return {result: resolved.result, source: resolved.source, attribution: resolved.attribution};
98
- }
106
+ export const findAddress = (query: string) => geocode({query, language: 'en'}, options);
107
+ export const reversePosition = (position: [number, number]) =>
108
+ geocodeReverse({position, kinds: ['address', 'place']}, options);
109
+ export const suggestPlaces = (query: string) => autocomplete({query, language: 'en'}, options);
110
+ export const resolveSelection = (token: string) => resolveSuggestion({token}, options);
99
111
  ```
100
112
 
101
- Call `resolveUserSelection` only after explicit selection. Do not automatically resolve every
102
- suggestion. Tokens are opaque and ephemeral; an expired token needs fresh suggestions. Suggestions
103
- do not guarantee a match for every place and are not category or discovery queries.
113
+ Text search and Nearby are distinct operations. A suggestion has a title, an optional one-line
114
+ `addressLabel` that tells same-named places apart, and no position. Resolve only the selected
115
+ suggestion. Reverse results are candidates in provider order, without containment, entrance
116
+ precision, routability, deliverability or address-validation guarantees.
104
117
 
105
- Debounce input and cancel superseded requests, or ignore stale results. The client owns no shared
106
- interaction state. Autocomplete rejects `retention`, returns no usage record, and consumes zero
107
- units. Each completed resolution reports one unit; replaying it is another operation, not a retry
108
- of an idempotent request.
118
+ To show positions, distances and categories while someone types, ask for each suggestion's place.
119
+ A completed request then uses 25 units instead of 10, and choosing a suggestion needs no further
120
+ resolution:
109
121
 
110
- ## Request limits and retention
122
+ <!-- docs:check -->
111
123
 
112
- Forward queries contain at most 200 characters. Forward and autocomplete `limit` default to 5 and
113
- accept 1–10. `bounds` and `proximity` are mutually exclusive. Bounds use
114
- `[west, south, east, north]` with west less than east and south less than north; do not pass a wrapped
115
- antimeridian box. Provider selection and raw provider options are not public controls.
124
+ ```ts
125
+ import {autocomplete} from '@tileflow/search';
126
+
127
+ const apiKey = process.env.TILEFLOW_API_KEY;
128
+ if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
129
+ const options = {apiKey};
116
130
 
117
- `retention` accepts `temporary` (default) or `persistent` for forward, reverse, and resolution.
118
- Temporary results are for immediate use, without a right granted by Tileflow to retain them
119
- permanently. Persistent mode requests compatible storage rights; it does not request or promise
120
- Tileflow storage. Only retain a resolved candidate with its source and attribution, not suggestion
121
- tokens or labels.
131
+ export async function suggestNearbyPlaces(query: string, proximity: [number, number]) {
132
+ const {suggestions} = await autocomplete({query, proximity, include: ['place']}, options);
133
+ return suggestions.map(({label, addressLabel, place}) => ({
134
+ label,
135
+ addressLabel,
136
+ position: place?.position,
137
+ distanceMeters: place?.distanceMeters,
138
+ }));
139
+ }
140
+ ```
122
141
 
123
- Persistent retention is unavailable in some territories. `GEOCODING_TERRITORY_UNSUPPORTED` (422)
124
- means the requested mode is unsupported, including AWS HERE-backed geocoding in Japan. It applies
125
- to forward, reverse, and resolution, not autocomplete. Follow the returned source terms rather
126
- than treating an SDK option as a blanket storage license.
142
+ ## Place details
127
143
 
128
- ## Handle failures and usage
144
+ Results carry an optional `distanceMeters` when the provider reports one: from `proximity`, from
145
+ the centre of `bounds`, or from the reverse or Nearby position. Places can also carry `foodTypes`.
146
+ Other details are requested with `include` and make the operation an extended one:
129
147
 
130
- Requests and normalized responses are strictly bounded. The client never retries automatically.
131
- Use `signal` to cancel work and `fetch` or `apiUrl` for tests. API URLs require HTTPS except for an
132
- explicit loopback HTTP origin during development.
148
+ | Detail | Returns | Available from |
149
+ | -------------- | ----------------------------------------------------------- | -------------------------------------------------------- |
150
+ | `contacts` | `phones`, `websites` and `emails`, each a `value` and label | Text search inside `bounds`, Nearby, explicit resolution |
151
+ | `openingHours` | Display lines per schedule and `openNow` at request time | Text search inside `bounds`, Nearby, explicit resolution |
152
+ | `timeZone` | IANA `name` and `utcOffsetSeconds` | Every place operation |
153
+ | `accessPoints` | Entrance or access positions, separate from `position` | Every place operation |
133
154
 
134
- Hosted errors throw `GeocodingError` with `status`, nullable `code`, and nullable `requestId`.
135
- Invalid local inputs and malformed remote documents can throw ordinary errors. A disabled service
136
- reports `GEOCODING_DISABLED`. Suggestion failures include `GEOCODING_INVALID_SUGGESTION` (400),
137
- `GEOCODING_SUGGESTION_EXPIRED` (410), `GEOCODING_UPSTREAM_THROTTLED` (429), and
138
- `GEOCODING_PICK_LIMIT_EXCEEDED` (429). Refresh an expired suggestion rather than replaying its token.
155
+ Asking geocoding without `bounds`, or reverse geocoding, for contacts or opening hours fails with
156
+ `GEOCODING_FILTER_UNSUPPORTED` before any request is sent. Each detail is optional in a response:
157
+ a place whose hours the provider does not supply has no `openingHours`.
139
158
 
140
- `GEOCODING_USAGE_UNCONFIRMED` means completion may already have been charged; do not automatically
141
- repeat the request. Each completed forward or reverse request reports one unit, including an empty
142
- result; completed resolution also reports one. These are usage units, not monetary prices.
159
+ <!-- docs:check -->
160
+
161
+ ```ts
162
+ import {resolvePlace} from '@tileflow/search';
163
+
164
+ const apiKey = process.env.TILEFLOW_API_KEY;
165
+ if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
166
+ const options = {apiKey};
167
+
168
+ export async function placeCard(token: string) {
169
+ const {result} = await resolvePlace({token, include: ['contacts', 'openingHours']}, options);
170
+ const website = result.contacts?.websites?.find(({value}) => /^https?:\/\//iu.test(value));
171
+ return {
172
+ name: result.name ?? result.label,
173
+ openNow: result.openingHours?.some(({openNow}) => openNow) ?? false,
174
+ hours: result.openingHours?.flatMap(({display}) => display ?? []) ?? [],
175
+ phone: result.contacts?.phones?.[0]?.value,
176
+ website: website?.value,
177
+ };
178
+ }
179
+ ```
143
180
 
144
- Use `@tileflow/search/contract` for schemas and `@tileflow/search/client` for transport. For a
145
- published release, its installed README and declarations take precedence over newer source on `main`.
181
+ Contact values are provider text, not validated links: check the scheme before rendering one as
182
+ a link, and expect several phones or websites for a place, not all equally relevant. `openNow`
183
+ describes the moment of the request. Like every result, details are for temporary display.
184
+
185
+ ## Limits and temporary use
186
+
187
+ Forward/autocomplete text accepts 1–200 characters. Their limit defaults to 5 and accepts 1–10;
188
+ reverse defaults to 1 and accepts 1–10. Reverse kinds are an OR of `address`, `street`, `locality`
189
+ and `place`. Forward/autocomplete `bounds` and `proximity` are mutually exclusive.
190
+
191
+ Nearby requires `position`, defaults to 20 results and accepts 1–100. Optional `radiusMeters` accepts
192
+ 1–21,000,000. A bbox is `[west, south, east, north]`, with west < east and south < north; wrapping
193
+ antimeridian boxes are unsupported. Position alone uses native provider scope and makes no hard
194
+ extent promise. Combined radius+bbox is currently rejected with `GEOCODING_FILTER_UNSUPPORTED`.
195
+
196
+ Use literal current category IDs, including spaces and punctuation. Include/exclude lists accept up
197
+ to ten IDs each. Inclusion is OR; exclusion vetoes matches. No parent-category expansion is implied.
198
+ Country filters accept up to 100 ISO alpha-2/alpha-3 codes and intersect spatial/category constraints.
199
+ Unknown IDs or countries are rejected. Language requests use provider fallback, without a translation
200
+ or localization completeness guarantee. Nearby does not accept text queries or an ordering selector.
201
+
202
+ Nearby requests are bounded to 32 KiB and responses to 512 KiB. Cursors accept at most 8192 characters;
203
+ selection tokens at most 2048. Oversized/invalid pages fail as a whole. The SDK parses strict schemas
204
+ and never drops malformed results to manufacture a valid success.
205
+
206
+ Forward, reverse and resolution accept only `retention: 'temporary'`, also the default. Persistent
207
+ retention, Advanced features and caller-selected provider options are rejected before provider work.
208
+
209
+ ## Usage and errors
210
+
211
+ | Completed operation | Shared API units | With `include` |
212
+ | ------------------------------------- | ---------------: | -------------: |
213
+ | Category discovery | 0 | — |
214
+ | Autocomplete | 10 | 25 |
215
+ | Forward, reverse, explicit resolution | 25 | 75 |
216
+ | Each Nearby page | 25 | 75 |
217
+
218
+ `searchOperationUnits(operation, include)` returns the same weights. Valid empty responses consume
219
+ the same units. Three autocomplete calls and one resolution consume 55 units. These are part of Starter's shared API allowance, with no separate Search credits or
220
+ commercial search session. Hovering, displaying results and moving a map do not call Search.
221
+
222
+ `GeocodingError` exposes `status`, nullable `code`, nullable `requestId` and nullable bounded
223
+ `retryAfterSeconds`. The client never retries, including after throttling. Use `signal` for
224
+ cancellation. Network failures can leave server completion uncertain.
225
+
226
+ | Code | Action |
227
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
228
+ | `GEOCODING_PLAN_UNAVAILABLE` | Use paid Starter or request an eligible evaluation. |
229
+ | `GEOCODING_EVALUATION_REQUIRED` / `GEOCODING_EVALUATION_EXPIRED` | Ask the Team's operator to review evaluation access. |
230
+ | `GEOCODING_EVALUATION_EXHAUSTED` | Stop requests; review evaluation limits. |
231
+ | `GEOCODING_PICK_LIMIT_EXCEEDED` / `GEOCODING_UPSTREAM_THROTTLED` | Respect `retryAfterSeconds`; retry only deliberately. |
232
+ | `GEOCODING_INVALID_CURSOR` / `GEOCODING_INVALID_SUGGESTION` | Check the Team and original query; start a fresh search explicitly. |
233
+ | `GEOCODING_CURSOR_EXPIRED` / `GEOCODING_SUGGESTION_EXPIRED` | Request fresh results explicitly. |
234
+ | `GEOCODING_USAGE_UNCONFIRMED` | Completion may already be charged. Inspect usage before repeating. |
235
+ | `GEOCODING_DISABLED` | Search is disabled; installation does not activate it. |
236
+
237
+ Rejected or failed operations do not return a completed usage record. Never translate an unconfirmed
238
+ completion into zero units. Confirmed server completion is not undone by a later disconnection.
239
+ API URLs require HTTPS, except explicit loopback HTTP during development. `fetch` can be injected
240
+ for local tests. Request and response schemas are exported from `@tileflow/search/contract`;
241
+ transport is available from `@tileflow/search/client`.
242
+
243
+ ## Integrate an independent search interaction
244
+
245
+ Each widget owns its timer, abort controller, request generation and selection state. Debounce input;
246
+ suppress requests while IME composition is active; launch the final input on `compositionend`.
247
+ Increment the generation and abort previous work on input, selection, dismissal or a view change.
248
+ Apply a response only if its generation still matches, even if abort did not stop the transport.
249
+
250
+ Use an accessible combobox/listbox for suggestions: arrow keys change the active option, Enter or a
251
+ touch/click explicitly selects it, and Escape dismisses it. Give the input a label, maintain focus
252
+ and active-option semantics, and announce loading, empty results and errors through a polite status
253
+ region. Preserve attribution beside displayed results. Do not resolve on hover or focus changes.
254
+
255
+ An explicit category action can call Nearby. Moving a map offers a “Search this area” button; only
256
+ pressing it submits the new bounds. A late reply must not move the camera after a newer selection or
257
+ view change. Keep independent generations for independent widgets. The SDK's tests demonstrate
258
+ cancellation and stale-suggestion handling; it does not supply a UI component or client credentials.
259
+
260
+ For a published package, its installed README and declarations describe that release; a link to
261
+ GitHub `main` can describe newer source.
@@ -5,13 +5,26 @@ import {
5
5
  geocodingForwardResponseSchema,
6
6
  geocodingLimits,
7
7
  geocodingReverseRequestSchema,
8
+ nearbyLimits,
9
+ nearbyRequestSchema,
10
+ nearbyResponseSchema,
8
11
  resolveSuggestionRequestSchema,
9
- resolveSuggestionResponseSchema
10
- } from "./chunk-EDYCZVXI.js";
12
+ resolveSuggestionResponseSchema,
13
+ searchCategoriesResponseSchema,
14
+ searchContactDetails,
15
+ searchOperationUnits
16
+ } from "./chunk-W66J2DAS.js";
11
17
 
12
18
  // src/client.ts
13
19
  import { z } from "zod";
14
20
  var GEOCODING_ERROR_CODES = [
21
+ "GEOCODING_FILTER_UNSUPPORTED",
22
+ "GEOCODING_PLAN_UNAVAILABLE",
23
+ "GEOCODING_EVALUATION_REQUIRED",
24
+ "GEOCODING_EVALUATION_EXPIRED",
25
+ "GEOCODING_EVALUATION_EXHAUSTED",
26
+ "GEOCODING_INVALID_CURSOR",
27
+ "GEOCODING_CURSOR_EXPIRED",
15
28
  "GEOCODING_ABORTED",
16
29
  "GEOCODING_DISABLED",
17
30
  "GEOCODING_INVALID_REQUEST",
@@ -20,7 +33,6 @@ var GEOCODING_ERROR_CODES = [
20
33
  "GEOCODING_QUOTA_EXCEEDED",
21
34
  "GEOCODING_REQUEST_TOO_LARGE",
22
35
  "GEOCODING_SUGGESTION_EXPIRED",
23
- "GEOCODING_TERRITORY_UNSUPPORTED",
24
36
  "GEOCODING_UNAVAILABLE",
25
37
  "GEOCODING_UPSTREAM_INVALID",
26
38
  "GEOCODING_UPSTREAM_THROTTLED",
@@ -29,6 +41,13 @@ var GEOCODING_ERROR_CODES = [
29
41
  "GEOCODING_USAGE_UNCONFIRMED"
30
42
  ];
31
43
  var geocodingErrorStatuses = {
44
+ GEOCODING_FILTER_UNSUPPORTED: 422,
45
+ GEOCODING_PLAN_UNAVAILABLE: 403,
46
+ GEOCODING_EVALUATION_REQUIRED: 403,
47
+ GEOCODING_EVALUATION_EXPIRED: 403,
48
+ GEOCODING_EVALUATION_EXHAUSTED: 429,
49
+ GEOCODING_INVALID_CURSOR: 400,
50
+ GEOCODING_CURSOR_EXPIRED: 410,
32
51
  GEOCODING_ABORTED: 499,
33
52
  GEOCODING_DISABLED: 503,
34
53
  GEOCODING_INVALID_REQUEST: 400,
@@ -37,7 +56,6 @@ var geocodingErrorStatuses = {
37
56
  GEOCODING_QUOTA_EXCEEDED: 429,
38
57
  GEOCODING_REQUEST_TOO_LARGE: 413,
39
58
  GEOCODING_SUGGESTION_EXPIRED: 410,
40
- GEOCODING_TERRITORY_UNSUPPORTED: 422,
41
59
  GEOCODING_UNAVAILABLE: 503,
42
60
  GEOCODING_UPSTREAM_INVALID: 502,
43
61
  GEOCODING_UPSTREAM_THROTTLED: 429,
@@ -60,12 +78,14 @@ var GeocodingError = class extends Error {
60
78
  code;
61
79
  requestId;
62
80
  status;
81
+ retryAfterSeconds;
63
82
  constructor(message, input) {
64
83
  super(message);
65
84
  this.name = "GeocodingError";
66
85
  this.code = input.code ?? null;
67
86
  this.requestId = input.requestId ?? null;
68
87
  this.status = input.status;
88
+ this.retryAfterSeconds = input.retryAfterSeconds ?? null;
69
89
  }
70
90
  };
71
91
  async function geocode(request, options) {
@@ -73,12 +93,14 @@ async function geocode(request, options) {
73
93
  if (!parsedRequest.success) {
74
94
  throw new Error("Invalid Tileflow geocoding request");
75
95
  }
96
+ if (!parsedRequest.data.bounds) rejectContactDetails(parsedRequest.data.include);
97
+ const units = searchOperationUnits("forward", parsedRequest.data.include);
76
98
  return requestGeocoding(
77
99
  "/v1/geocoding/forward",
78
100
  parsedRequest.data,
79
101
  options,
80
102
  geocodingForwardResponseSchema,
81
- (response) => response.results.length <= (parsedRequest.data.limit ?? geocodingLimits.defaultLimit)
103
+ (response) => response.usage.units === units && response.results.length <= (parsedRequest.data.limit ?? geocodingLimits.defaultLimit)
82
104
  );
83
105
  }
84
106
  async function geocodeReverse(request, options) {
@@ -86,12 +108,14 @@ async function geocodeReverse(request, options) {
86
108
  if (!parsedRequest.success) {
87
109
  throw new Error("Invalid Tileflow geocoding request");
88
110
  }
111
+ rejectContactDetails(parsedRequest.data.include);
112
+ const units = searchOperationUnits("reverse", parsedRequest.data.include);
89
113
  return requestGeocoding(
90
114
  "/v1/geocoding/reverse",
91
115
  parsedRequest.data,
92
116
  options,
93
117
  geocodingForwardResponseSchema,
94
- (response) => response.results.length <= parsedRequest.data.limit && containsOnlyKinds(response, parsedRequest.data.kinds)
118
+ (response) => response.usage.units === units && response.results.length <= parsedRequest.data.limit && containsOnlyKinds(response, parsedRequest.data.kinds)
95
119
  );
96
120
  }
97
121
  async function autocomplete(request, options) {
@@ -99,12 +123,13 @@ async function autocomplete(request, options) {
99
123
  if (!parsedRequest.success) {
100
124
  throw new Error("Invalid Tileflow geocoding request");
101
125
  }
126
+ const withPlaces = parsedRequest.data.include !== void 0;
102
127
  return requestGeocoding(
103
128
  "/v1/geocoding/autocomplete",
104
129
  parsedRequest.data,
105
130
  options,
106
131
  autocompleteResponseSchema,
107
- (response) => response.suggestions.length <= parsedRequest.data.limit
132
+ (response) => response.usage.units === searchOperationUnits("autocomplete", parsedRequest.data.include) && response.suggestions.length <= parsedRequest.data.limit && response.suggestions.every((suggestion) => suggestion.place !== void 0 === withPlaces)
108
133
  );
109
134
  }
110
135
  async function resolveSuggestion(request, options) {
@@ -112,27 +137,64 @@ async function resolveSuggestion(request, options) {
112
137
  if (!parsedRequest.success) {
113
138
  throw new Error("Invalid Tileflow geocoding request");
114
139
  }
140
+ const units = searchOperationUnits("resolve", parsedRequest.data.include);
115
141
  return requestGeocoding(
116
- "/v1/geocoding/resolve-suggestion",
142
+ "/v1/geocoding/resolve",
117
143
  parsedRequest.data,
118
144
  options,
119
- resolveSuggestionResponseSchema
145
+ resolveSuggestionResponseSchema,
146
+ (response) => response.usage.units === units
120
147
  );
121
148
  }
122
- async function requestGeocoding(path, request, options, responseSchema, isExpectedResponse = () => true) {
149
+ var resolvePlace = resolveSuggestion;
150
+ async function searchNearby(request, options) {
151
+ const parsed = nearbyRequestSchema.safeParse(request);
152
+ if (!parsed.success) throw new Error("Invalid Tileflow Nearby request");
153
+ const query = parsed.data;
154
+ if (query.radiusMeters !== void 0 && query.bounds !== void 0 || query.includeCategories?.some((id) => query.excludeCategories?.includes(id))) {
155
+ throw new GeocodingError("Unsupported geocoding filters", {
156
+ code: "GEOCODING_FILTER_UNSUPPORTED",
157
+ status: 422
158
+ });
159
+ }
160
+ const units = searchOperationUnits("nearby", query.include);
161
+ return requestGeocoding(
162
+ "/v1/geocoding/nearby",
163
+ query,
164
+ options,
165
+ nearbyResponseSchema,
166
+ (response) => response.usage.units === units && response.results.length <= query.limit,
167
+ nearbyLimits.maximumResponseBytes
168
+ );
169
+ }
170
+ function listCategories(options) {
171
+ return requestGeocoding(
172
+ "/v1/geocoding/categories",
173
+ void 0,
174
+ options,
175
+ searchCategoriesResponseSchema,
176
+ () => true,
177
+ nearbyLimits.maximumResponseBytes
178
+ );
179
+ }
180
+ async function requestGeocoding(path, request, options, responseSchema, isExpectedResponse = () => true, maximumResponseBytes = geocodingLimits.maximumResponseBytes) {
181
+ const bodyText = request === void 0 ? void 0 : JSON.stringify(request);
182
+ const maximumRequestBytes = path === "/v1/geocoding/nearby" ? nearbyLimits.maximumRequestBytes : 8 * 1024;
183
+ if (bodyText && new TextEncoder().encode(bodyText).byteLength > maximumRequestBytes)
184
+ throw new Error("Tileflow Search request is too large");
123
185
  const apiKey = normalizeApiKey(options.apiKey);
124
186
  const apiUrl = normalizeApiUrl(options.apiUrl ?? "https://api.tileflow.dev");
125
187
  const fetcher = options.fetch ?? fetch;
126
188
  let response;
127
189
  try {
128
190
  response = await fetcher(`${apiUrl}${path}`, {
129
- body: JSON.stringify(request),
191
+ ...bodyText === void 0 ? {} : { body: bodyText },
130
192
  headers: {
131
193
  Accept: "application/json",
132
194
  Authorization: `Bearer ${apiKey}`,
133
195
  "Content-Type": "application/json"
134
196
  },
135
- method: "POST",
197
+ method: request === void 0 ? "GET" : "POST",
136
198
  redirect: "error",
137
199
  signal: options.signal
138
200
  });
@@ -149,7 +211,7 @@ async function requestGeocoding(path, request, options, responseSchema, isExpect
149
211
  }
150
212
  let body;
151
213
  try {
152
- body = await readBoundedJson(response, geocodingLimits.maximumResponseBytes, options.signal);
214
+ body = await readBoundedJson(response, maximumResponseBytes, options.signal);
153
215
  } catch (error) {
154
216
  if (options.signal?.aborted) throw options.signal.reason ?? error;
155
217
  throw new GeocodingError("Tileflow geocoding returned an invalid response", {
@@ -164,6 +226,14 @@ async function requestGeocoding(path, request, options, responseSchema, isExpect
164
226
  }
165
227
  return parsedResponse.data;
166
228
  }
229
+ function rejectContactDetails(include) {
230
+ if (include?.some((detail) => searchContactDetails.includes(detail))) {
231
+ throw new GeocodingError("Unsupported geocoding filters", {
232
+ code: "GEOCODING_FILTER_UNSUPPORTED",
233
+ status: 422
234
+ });
235
+ }
236
+ }
167
237
  function containsOnlyKinds(response, allowedKinds) {
168
238
  if (!allowedKinds) return true;
169
239
  const kindFilter = new Set(allowedKinds);
@@ -189,18 +259,25 @@ function normalizeApiUrl(value) {
189
259
  return url.href.replace(/\/$/u, "");
190
260
  }
191
261
  async function readGeocodingError(response, signal) {
262
+ const value = response.headers.get("Retry-After");
263
+ const delay = value && /^\d{1,8}$/u.test(value) ? Number(value) : null;
264
+ const retryAfterSeconds = delay !== null && delay <= 31536e3 ? delay : null;
192
265
  let body;
193
266
  try {
194
267
  body = await readBoundedJson(response, geocodingLimits.maximumSafeErrorBytes, signal);
195
268
  } catch (error) {
196
269
  if (signal?.aborted) throw signal.reason ?? error;
197
- return new GeocodingError("Tileflow geocoding request failed", { status: response.status });
270
+ return new GeocodingError("Tileflow geocoding request failed", {
271
+ retryAfterSeconds,
272
+ status: response.status
273
+ });
198
274
  }
199
275
  const geocoding = geocodingErrorResponseSchema.safeParse(body);
200
276
  if (geocoding.success && geocodingErrorStatuses[geocoding.data.code] === response.status) {
201
277
  return new GeocodingError(geocoding.data.error, {
202
278
  code: geocoding.data.code,
203
279
  requestId: geocoding.data.requestId,
280
+ retryAfterSeconds,
204
281
  status: response.status
205
282
  });
206
283
  }
@@ -208,10 +285,14 @@ async function readGeocodingError(response, signal) {
208
285
  if (credential.success && [401, 403, 429].includes(response.status)) {
209
286
  return new GeocodingError(credential.data.error, {
210
287
  requestId: credential.data.requestId,
288
+ retryAfterSeconds,
211
289
  status: response.status
212
290
  });
213
291
  }
214
- return new GeocodingError("Tileflow geocoding request failed", { status: response.status });
292
+ return new GeocodingError("Tileflow geocoding request failed", {
293
+ retryAfterSeconds,
294
+ status: response.status
295
+ });
215
296
  }
216
297
  async function readBoundedJson(response, maximumBytes, signal) {
217
298
  const declaredLength = response.headers.get("Content-Length");
@@ -282,5 +363,8 @@ export {
282
363
  geocode,
283
364
  geocodeReverse,
284
365
  autocomplete,
285
- resolveSuggestion
366
+ resolveSuggestion,
367
+ resolvePlace,
368
+ searchNearby,
369
+ listCategories
286
370
  };