@tileflow/search 0.1.0-alpha.3 → 0.1.0-alpha.5
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 +139 -29
- package/dist/{chunk-FEWG4I4E.js → chunk-7PHOZFMS.js} +49 -8
- package/dist/{chunk-R7KE2IN7.js → chunk-W7IIJLOI.js} +69 -7
- package/dist/client.d.ts +9 -1
- package/dist/client.js +2 -2
- package/dist/contract.d.ts +583 -90
- package/dist/contract.js +23 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,14 +11,43 @@ selected-place resolution. It uses `fetch` and has no renderer or global interac
|
|
|
11
11
|
npm install @tileflow/search@alpha
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Use Node.js 22 or another runtime with `fetch`. Live requests require enabled Tileflow
|
|
15
|
-
Team credential with `geocoding:read
|
|
16
|
-
|
|
14
|
+
Use Node.js 22, a browser or another runtime with `fetch`. Live requests require enabled Tileflow
|
|
15
|
+
Search and a Team credential with `geocoding:read`: a publishable credential in websites and apps,
|
|
16
|
+
or a server credential on your server.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Free Teams, Starter trials and complimentary access run Search within their period's API units, up
|
|
19
|
+
to 100,000 Search units, while Search without payment is open; a finite evaluation grant can extend
|
|
20
|
+
them. Paid Starter runs Search within its shared API allowance and excess.
|
|
20
21
|
Installing this package does not enable the service or grant access.
|
|
21
22
|
|
|
23
|
+
## Call Search from a browser or app
|
|
24
|
+
|
|
25
|
+
A publishable credential (`tf_public_…`) is meant to be visible in client code. It grants Search
|
|
26
|
+
only, works only from the websites and native apps listed for it, and always carries per-minute and
|
|
27
|
+
per-day quotas. A Team Admin creates one under **Credentials** in the Tileflow dashboard.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import {autocomplete} from '@tileflow/search';
|
|
31
|
+
|
|
32
|
+
const options = {apiKey: 'tf_public_…'};
|
|
33
|
+
const page = await autocomplete({query: 'Hospital La Paz', proximity: [-3.7, 40.42]}, options);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Browsers send the page's origin, which must match one of the credential's websites exactly: scheme,
|
|
37
|
+
host and port. Native apps send no origin, so name the app instead. The SDK then sends its bundle ID
|
|
38
|
+
or package name and, on Android, the SHA-256 fingerprint of its signing certificate:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const options = {
|
|
42
|
+
apiKey: 'tf_public_…',
|
|
43
|
+
app: {id: 'com.example.store', certificateSha256: '14:6D:E9:…'},
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Anyone can read a publishable credential and imitate an origin or app outside a browser, so these
|
|
48
|
+
restrictions limit casual reuse rather than prove identity. Its quotas and the Team's spending limit
|
|
49
|
+
bound what a copied credential can consume. Keep server credentials (`tf_live_…`) on your server.
|
|
50
|
+
|
|
22
51
|
## Find nearby places
|
|
23
52
|
|
|
24
53
|
Run this example on your server with `TILEFLOW_API_KEY` set securely. It lists the current categories,
|
|
@@ -63,7 +92,7 @@ are temporary. Do not build a permanent place catalog, cache results, or persist
|
|
|
63
92
|
|
|
64
93
|
Save the original request in the current interaction. Another page repeats that request with the
|
|
65
94
|
returned `nextCursor`; no page is fetched automatically. A successful empty page can have a cursor.
|
|
66
|
-
Each explicit page costs
|
|
95
|
+
Each explicit page costs 10 API units. There is no total-count or snapshot-consistency promise.
|
|
67
96
|
|
|
68
97
|
<!-- docs:check -->
|
|
69
98
|
|
|
@@ -110,15 +139,84 @@ export const suggestPlaces = (query: string) => autocomplete({query, language: '
|
|
|
110
139
|
export const resolveSelection = (token: string) => resolveSuggestion({token}, options);
|
|
111
140
|
```
|
|
112
141
|
|
|
113
|
-
Text search and Nearby are distinct operations.
|
|
114
|
-
|
|
115
|
-
|
|
142
|
+
Text search and Nearby are distinct operations. A suggestion has a title, an optional one-line
|
|
143
|
+
`addressLabel` that tells same-named places apart, and no position. Resolve only the selected
|
|
144
|
+
suggestion. Reverse results are candidates in provider order, without containment, entrance
|
|
145
|
+
precision, routability, deliverability or address-validation guarantees.
|
|
146
|
+
|
|
147
|
+
To show positions, distances and categories while someone types, ask for each suggestion's place.
|
|
148
|
+
A completed request then uses 10 units instead of 4, and choosing a suggestion needs no further
|
|
149
|
+
resolution:
|
|
150
|
+
|
|
151
|
+
<!-- docs:check -->
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import {autocomplete} from '@tileflow/search';
|
|
155
|
+
|
|
156
|
+
const apiKey = process.env.TILEFLOW_API_KEY;
|
|
157
|
+
if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
|
|
158
|
+
const options = {apiKey};
|
|
159
|
+
|
|
160
|
+
export async function suggestNearbyPlaces(query: string, proximity: [number, number]) {
|
|
161
|
+
const {suggestions} = await autocomplete({query, proximity, include: ['place']}, options);
|
|
162
|
+
return suggestions.map(({label, addressLabel, place}) => ({
|
|
163
|
+
label,
|
|
164
|
+
addressLabel,
|
|
165
|
+
position: place?.position,
|
|
166
|
+
distanceMeters: place?.distanceMeters,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Place details
|
|
172
|
+
|
|
173
|
+
Results carry an optional `distanceMeters` when the provider reports one: from `proximity`, from
|
|
174
|
+
the centre of `bounds`, or from the reverse or Nearby position. Places can also carry `foodTypes`.
|
|
175
|
+
Other details are requested with `include` and make the operation an extended one:
|
|
176
|
+
|
|
177
|
+
| Detail | Returns | Available from |
|
|
178
|
+
| -------------- | ----------------------------------------------------------- | -------------------------------------------------------- |
|
|
179
|
+
| `contacts` | `phones`, `websites` and `emails`, each a `value` and label | Text search inside `bounds`, Nearby, explicit resolution |
|
|
180
|
+
| `openingHours` | Display lines per schedule and `openNow` at request time | Text search inside `bounds`, Nearby, explicit resolution |
|
|
181
|
+
| `timeZone` | IANA `name` and `utcOffsetSeconds` | Every place operation |
|
|
182
|
+
| `accessPoints` | Entrance or access positions, separate from `position` | Every place operation |
|
|
183
|
+
|
|
184
|
+
Asking geocoding without `bounds`, or reverse geocoding, for contacts or opening hours fails with
|
|
185
|
+
`GEOCODING_FILTER_UNSUPPORTED` before any request is sent. Each detail is optional in a response:
|
|
186
|
+
a place whose hours the provider does not supply has no `openingHours`.
|
|
187
|
+
|
|
188
|
+
<!-- docs:check -->
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import {resolvePlace} from '@tileflow/search';
|
|
192
|
+
|
|
193
|
+
const apiKey = process.env.TILEFLOW_API_KEY;
|
|
194
|
+
if (!apiKey) throw new Error('Set TILEFLOW_API_KEY on the server.');
|
|
195
|
+
const options = {apiKey};
|
|
196
|
+
|
|
197
|
+
export async function placeCard(token: string) {
|
|
198
|
+
const {result} = await resolvePlace({token, include: ['contacts', 'openingHours']}, options);
|
|
199
|
+
const website = result.contacts?.websites?.find(({value}) => /^https?:\/\//iu.test(value));
|
|
200
|
+
return {
|
|
201
|
+
name: result.name ?? result.label,
|
|
202
|
+
openNow: result.openingHours?.some(({openNow}) => openNow) ?? false,
|
|
203
|
+
hours: result.openingHours?.flatMap(({display}) => display ?? []) ?? [],
|
|
204
|
+
phone: result.contacts?.phones?.[0]?.value,
|
|
205
|
+
website: website?.value,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Contact values are provider text, not validated links: check the scheme before rendering one as
|
|
211
|
+
a link, and expect several phones or websites for a place, not all equally relevant. `openNow`
|
|
212
|
+
describes the moment of the request. Like every result, details are for temporary display.
|
|
116
213
|
|
|
117
214
|
## Limits and temporary use
|
|
118
215
|
|
|
119
216
|
Forward/autocomplete text accepts 1–200 characters. Their limit defaults to 5 and accepts 1–10;
|
|
120
217
|
reverse defaults to 1 and accepts 1–10. Reverse kinds are an OR of `address`, `street`, `locality`
|
|
121
|
-
and `place`. Forward/autocomplete `
|
|
218
|
+
and `place`. Forward/autocomplete `proximity` biases ranking and `bounds` restricts results; they
|
|
219
|
+
are mutually exclusive.
|
|
122
220
|
|
|
123
221
|
Nearby requires `position`, defaults to 20 results and accepts 1–100. Optional `radiusMeters` accepts
|
|
124
222
|
1–21,000,000. A bbox is `[west, south, east, north]`, with west < east and south < north; wrapping
|
|
@@ -140,31 +238,37 @@ retention, Advanced features and caller-selected provider options are rejected b
|
|
|
140
238
|
|
|
141
239
|
## Usage and errors
|
|
142
240
|
|
|
143
|
-
| Completed operation | Shared API units |
|
|
144
|
-
| ------------------------------------- | ---------------: |
|
|
145
|
-
| Category discovery | 0 |
|
|
146
|
-
| Autocomplete |
|
|
147
|
-
| Forward, reverse, explicit resolution |
|
|
148
|
-
| Each Nearby page |
|
|
241
|
+
| Completed operation | Shared API units | With `include` |
|
|
242
|
+
| ------------------------------------- | ---------------: | -------------: |
|
|
243
|
+
| Category discovery | 0 | — |
|
|
244
|
+
| Autocomplete | 4 | 10 |
|
|
245
|
+
| Forward, reverse, explicit resolution | 10 | 30 |
|
|
246
|
+
| Each Nearby page | 10 | 30 |
|
|
149
247
|
|
|
150
|
-
|
|
151
|
-
|
|
248
|
+
`searchOperationUnits(operation, include)` returns the same weights. Valid empty responses consume
|
|
249
|
+
the same units. Three autocomplete calls and one resolution consume 22 units. These are part of Starter's shared API allowance, with no separate Search credits or
|
|
152
250
|
commercial search session. Hovering, displaying results and moving a map do not call Search.
|
|
153
251
|
|
|
154
252
|
`GeocodingError` exposes `status`, nullable `code`, nullable `requestId` and nullable bounded
|
|
155
253
|
`retryAfterSeconds`. The client never retries, including after throttling. Use `signal` for
|
|
156
254
|
cancellation. Network failures can leave server completion uncertain.
|
|
157
255
|
|
|
158
|
-
| Code | Action
|
|
159
|
-
| ---------------------------------------------------------------- |
|
|
160
|
-
| `GEOCODING_PLAN_UNAVAILABLE` | Use paid Starter or request an eligible evaluation.
|
|
161
|
-
| `GEOCODING_EVALUATION_REQUIRED` / `GEOCODING_EVALUATION_EXPIRED` | Ask the Team's operator to review evaluation access.
|
|
162
|
-
| `GEOCODING_EVALUATION_EXHAUSTED` | Stop requests; review evaluation limits.
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
256
|
+
| Code | Action |
|
|
257
|
+
| ---------------------------------------------------------------- | --------------------------------------------------------------------- |
|
|
258
|
+
| `GEOCODING_PLAN_UNAVAILABLE` | Use paid Starter or request an eligible evaluation. |
|
|
259
|
+
| `GEOCODING_EVALUATION_REQUIRED` / `GEOCODING_EVALUATION_EXPIRED` | Ask the Team's operator to review evaluation access. |
|
|
260
|
+
| `GEOCODING_EVALUATION_EXHAUSTED` | Stop requests; review evaluation limits. |
|
|
261
|
+
| `GEOCODING_UNPAID_LIMIT_REACHED` | Search without payment reached its period limit; upgrade to Starter. |
|
|
262
|
+
| `GEOCODING_UNPAID_BUDGET_EXHAUSTED` | Search without payment is paused; retry after `retryAfterSeconds`. |
|
|
263
|
+
| `GEOCODING_SUSPENDED` | Search is suspended for the Team; contact support. |
|
|
264
|
+
| `GEOCODING_SPENDING_LIMIT_REACHED` | The Team reached its spending limit; raise it or wait for the period. |
|
|
265
|
+
| `GEOCODING_CREDENTIAL_QUOTA_EXCEEDED` | The credential reached its quota; respect `retryAfterSeconds`. |
|
|
266
|
+
| `GEOCODING_ORIGIN_NOT_ALLOWED` | Add this website or app to the publishable credential. |
|
|
267
|
+
| `GEOCODING_PICK_LIMIT_EXCEEDED` / `GEOCODING_UPSTREAM_THROTTLED` | Respect `retryAfterSeconds`; retry only deliberately. |
|
|
268
|
+
| `GEOCODING_INVALID_CURSOR` / `GEOCODING_INVALID_SUGGESTION` | Check the Team and original query; start a fresh search explicitly. |
|
|
269
|
+
| `GEOCODING_CURSOR_EXPIRED` / `GEOCODING_SUGGESTION_EXPIRED` | Request fresh results explicitly. |
|
|
270
|
+
| `GEOCODING_USAGE_UNCONFIRMED` | Completion may already be charged. Inspect usage before repeating. |
|
|
271
|
+
| `GEOCODING_DISABLED` | Search is disabled; installation does not activate it. |
|
|
168
272
|
|
|
169
273
|
Rejected or failed operations do not return a completed usage record. Never translate an unconfirmed
|
|
170
274
|
completion into zero units. Confirmed server completion is not undone by a later disconnection.
|
|
@@ -184,10 +288,16 @@ touch/click explicitly selects it, and Escape dismisses it. Give the input a lab
|
|
|
184
288
|
and active-option semantics, and announce loading, empty results and errors through a polite status
|
|
185
289
|
region. Preserve attribution beside displayed results. Do not resolve on hover or focus changes.
|
|
186
290
|
|
|
291
|
+
Text search over a map should favor the view without being confined to it. Send the map's centre as
|
|
292
|
+
`proximity` while it shows a city or closer, and omit it at country or world scale; nearer matches
|
|
293
|
+
then rank first, and a better match elsewhere, such as the city of Madrid typed over Lisbon, still
|
|
294
|
+
appears. `bounds` excludes everything outside the box, so do not derive it from the view for typed
|
|
295
|
+
search.
|
|
296
|
+
|
|
187
297
|
An explicit category action can call Nearby. Moving a map offers a “Search this area” button; only
|
|
188
298
|
pressing it submits the new bounds. A late reply must not move the camera after a newer selection or
|
|
189
299
|
view change. Keep independent generations for independent widgets. The SDK's tests demonstrate
|
|
190
|
-
cancellation and stale-suggestion handling; it does not supply a UI component
|
|
300
|
+
cancellation and stale-suggestion handling; it does not supply a UI component.
|
|
191
301
|
|
|
192
302
|
For a published package, its installed README and declarations describe that release; a link to
|
|
193
303
|
GitHub `main` can describe newer source.
|
|
@@ -10,8 +10,10 @@ import {
|
|
|
10
10
|
nearbyResponseSchema,
|
|
11
11
|
resolveSuggestionRequestSchema,
|
|
12
12
|
resolveSuggestionResponseSchema,
|
|
13
|
-
searchCategoriesResponseSchema
|
|
14
|
-
|
|
13
|
+
searchCategoriesResponseSchema,
|
|
14
|
+
searchContactDetails,
|
|
15
|
+
searchOperationUnits
|
|
16
|
+
} from "./chunk-W7IIJLOI.js";
|
|
15
17
|
|
|
16
18
|
// src/client.ts
|
|
17
19
|
import { z } from "zod";
|
|
@@ -27,11 +29,17 @@ var GEOCODING_ERROR_CODES = [
|
|
|
27
29
|
"GEOCODING_DISABLED",
|
|
28
30
|
"GEOCODING_INVALID_REQUEST",
|
|
29
31
|
"GEOCODING_INVALID_SUGGESTION",
|
|
32
|
+
"GEOCODING_ORIGIN_NOT_ALLOWED",
|
|
30
33
|
"GEOCODING_PICK_LIMIT_EXCEEDED",
|
|
31
34
|
"GEOCODING_QUOTA_EXCEEDED",
|
|
32
35
|
"GEOCODING_REQUEST_TOO_LARGE",
|
|
36
|
+
"GEOCODING_SPENDING_LIMIT_REACHED",
|
|
37
|
+
"GEOCODING_CREDENTIAL_QUOTA_EXCEEDED",
|
|
33
38
|
"GEOCODING_SUGGESTION_EXPIRED",
|
|
39
|
+
"GEOCODING_SUSPENDED",
|
|
34
40
|
"GEOCODING_UNAVAILABLE",
|
|
41
|
+
"GEOCODING_UNPAID_BUDGET_EXHAUSTED",
|
|
42
|
+
"GEOCODING_UNPAID_LIMIT_REACHED",
|
|
35
43
|
"GEOCODING_UPSTREAM_INVALID",
|
|
36
44
|
"GEOCODING_UPSTREAM_THROTTLED",
|
|
37
45
|
"GEOCODING_UPSTREAM_TIMEOUT",
|
|
@@ -50,11 +58,17 @@ var geocodingErrorStatuses = {
|
|
|
50
58
|
GEOCODING_DISABLED: 503,
|
|
51
59
|
GEOCODING_INVALID_REQUEST: 400,
|
|
52
60
|
GEOCODING_INVALID_SUGGESTION: 400,
|
|
61
|
+
GEOCODING_ORIGIN_NOT_ALLOWED: 403,
|
|
53
62
|
GEOCODING_PICK_LIMIT_EXCEEDED: 429,
|
|
54
63
|
GEOCODING_QUOTA_EXCEEDED: 429,
|
|
55
64
|
GEOCODING_REQUEST_TOO_LARGE: 413,
|
|
65
|
+
GEOCODING_SPENDING_LIMIT_REACHED: 429,
|
|
66
|
+
GEOCODING_CREDENTIAL_QUOTA_EXCEEDED: 429,
|
|
56
67
|
GEOCODING_SUGGESTION_EXPIRED: 410,
|
|
68
|
+
GEOCODING_SUSPENDED: 403,
|
|
57
69
|
GEOCODING_UNAVAILABLE: 503,
|
|
70
|
+
GEOCODING_UNPAID_BUDGET_EXHAUSTED: 429,
|
|
71
|
+
GEOCODING_UNPAID_LIMIT_REACHED: 429,
|
|
58
72
|
GEOCODING_UPSTREAM_INVALID: 502,
|
|
59
73
|
GEOCODING_UPSTREAM_THROTTLED: 429,
|
|
60
74
|
GEOCODING_UPSTREAM_TIMEOUT: 504,
|
|
@@ -91,12 +105,14 @@ async function geocode(request, options) {
|
|
|
91
105
|
if (!parsedRequest.success) {
|
|
92
106
|
throw new Error("Invalid Tileflow geocoding request");
|
|
93
107
|
}
|
|
108
|
+
if (!parsedRequest.data.bounds) rejectContactDetails(parsedRequest.data.include);
|
|
109
|
+
const units = searchOperationUnits("forward", parsedRequest.data.include);
|
|
94
110
|
return requestGeocoding(
|
|
95
111
|
"/v1/geocoding/forward",
|
|
96
112
|
parsedRequest.data,
|
|
97
113
|
options,
|
|
98
114
|
geocodingForwardResponseSchema,
|
|
99
|
-
(response) => response.results.length <= (parsedRequest.data.limit ?? geocodingLimits.defaultLimit)
|
|
115
|
+
(response) => response.usage.units === units && response.results.length <= (parsedRequest.data.limit ?? geocodingLimits.defaultLimit)
|
|
100
116
|
);
|
|
101
117
|
}
|
|
102
118
|
async function geocodeReverse(request, options) {
|
|
@@ -104,12 +120,14 @@ async function geocodeReverse(request, options) {
|
|
|
104
120
|
if (!parsedRequest.success) {
|
|
105
121
|
throw new Error("Invalid Tileflow geocoding request");
|
|
106
122
|
}
|
|
123
|
+
rejectContactDetails(parsedRequest.data.include);
|
|
124
|
+
const units = searchOperationUnits("reverse", parsedRequest.data.include);
|
|
107
125
|
return requestGeocoding(
|
|
108
126
|
"/v1/geocoding/reverse",
|
|
109
127
|
parsedRequest.data,
|
|
110
128
|
options,
|
|
111
129
|
geocodingForwardResponseSchema,
|
|
112
|
-
(response) => response.results.length <= parsedRequest.data.limit && containsOnlyKinds(response, parsedRequest.data.kinds)
|
|
130
|
+
(response) => response.usage.units === units && response.results.length <= parsedRequest.data.limit && containsOnlyKinds(response, parsedRequest.data.kinds)
|
|
113
131
|
);
|
|
114
132
|
}
|
|
115
133
|
async function autocomplete(request, options) {
|
|
@@ -117,12 +135,13 @@ async function autocomplete(request, options) {
|
|
|
117
135
|
if (!parsedRequest.success) {
|
|
118
136
|
throw new Error("Invalid Tileflow geocoding request");
|
|
119
137
|
}
|
|
138
|
+
const withPlaces = parsedRequest.data.include !== void 0;
|
|
120
139
|
return requestGeocoding(
|
|
121
140
|
"/v1/geocoding/autocomplete",
|
|
122
141
|
parsedRequest.data,
|
|
123
142
|
options,
|
|
124
143
|
autocompleteResponseSchema,
|
|
125
|
-
(response) => response.suggestions.length <= parsedRequest.data.limit
|
|
144
|
+
(response) => response.usage.units === searchOperationUnits("autocomplete", parsedRequest.data.include) && response.suggestions.length <= parsedRequest.data.limit && response.suggestions.every((suggestion) => suggestion.place !== void 0 === withPlaces)
|
|
126
145
|
);
|
|
127
146
|
}
|
|
128
147
|
async function resolveSuggestion(request, options) {
|
|
@@ -130,11 +149,13 @@ async function resolveSuggestion(request, options) {
|
|
|
130
149
|
if (!parsedRequest.success) {
|
|
131
150
|
throw new Error("Invalid Tileflow geocoding request");
|
|
132
151
|
}
|
|
152
|
+
const units = searchOperationUnits("resolve", parsedRequest.data.include);
|
|
133
153
|
return requestGeocoding(
|
|
134
154
|
"/v1/geocoding/resolve",
|
|
135
155
|
parsedRequest.data,
|
|
136
156
|
options,
|
|
137
|
-
resolveSuggestionResponseSchema
|
|
157
|
+
resolveSuggestionResponseSchema,
|
|
158
|
+
(response) => response.usage.units === units
|
|
138
159
|
);
|
|
139
160
|
}
|
|
140
161
|
var resolvePlace = resolveSuggestion;
|
|
@@ -148,12 +169,13 @@ async function searchNearby(request, options) {
|
|
|
148
169
|
status: 422
|
|
149
170
|
});
|
|
150
171
|
}
|
|
172
|
+
const units = searchOperationUnits("nearby", query.include);
|
|
151
173
|
return requestGeocoding(
|
|
152
174
|
"/v1/geocoding/nearby",
|
|
153
175
|
query,
|
|
154
176
|
options,
|
|
155
177
|
nearbyResponseSchema,
|
|
156
|
-
(response) => response.results.length <= query.limit,
|
|
178
|
+
(response) => response.usage.units === units && response.results.length <= query.limit,
|
|
157
179
|
nearbyLimits.maximumResponseBytes
|
|
158
180
|
);
|
|
159
181
|
}
|
|
@@ -173,6 +195,7 @@ async function requestGeocoding(path, request, options, responseSchema, isExpect
|
|
|
173
195
|
if (bodyText && new TextEncoder().encode(bodyText).byteLength > maximumRequestBytes)
|
|
174
196
|
throw new Error("Tileflow Search request is too large");
|
|
175
197
|
const apiKey = normalizeApiKey(options.apiKey);
|
|
198
|
+
const app = normalizeApp(options.app);
|
|
176
199
|
const apiUrl = normalizeApiUrl(options.apiUrl ?? "https://api.tileflow.dev");
|
|
177
200
|
const fetcher = options.fetch ?? fetch;
|
|
178
201
|
let response;
|
|
@@ -182,7 +205,9 @@ async function requestGeocoding(path, request, options, responseSchema, isExpect
|
|
|
182
205
|
headers: {
|
|
183
206
|
Accept: "application/json",
|
|
184
207
|
Authorization: `Bearer ${apiKey}`,
|
|
185
|
-
"Content-Type": "application/json"
|
|
208
|
+
"Content-Type": "application/json",
|
|
209
|
+
...app ? { "X-Tileflow-App": app.id } : {},
|
|
210
|
+
...app?.certificateSha256 ? { "X-Tileflow-App-Certificate": app.certificateSha256 } : {}
|
|
186
211
|
},
|
|
187
212
|
method: request === void 0 ? "GET" : "POST",
|
|
188
213
|
redirect: "error",
|
|
@@ -216,11 +241,27 @@ async function requestGeocoding(path, request, options, responseSchema, isExpect
|
|
|
216
241
|
}
|
|
217
242
|
return parsedResponse.data;
|
|
218
243
|
}
|
|
244
|
+
function rejectContactDetails(include) {
|
|
245
|
+
if (include?.some((detail) => searchContactDetails.includes(detail))) {
|
|
246
|
+
throw new GeocodingError("Unsupported geocoding filters", {
|
|
247
|
+
code: "GEOCODING_FILTER_UNSUPPORTED",
|
|
248
|
+
status: 422
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
219
252
|
function containsOnlyKinds(response, allowedKinds) {
|
|
220
253
|
if (!allowedKinds) return true;
|
|
221
254
|
const kindFilter = new Set(allowedKinds);
|
|
222
255
|
return response.results.every(({ kind }) => kindFilter.has(kind));
|
|
223
256
|
}
|
|
257
|
+
function normalizeApp(value) {
|
|
258
|
+
if (value === void 0) return null;
|
|
259
|
+
const certificate = value?.certificateSha256?.replaceAll(":", "").toLowerCase();
|
|
260
|
+
if (typeof value?.id !== "string" || value.id.length > 255 || !/^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z0-9_-]+)+$/u.test(value.id) || certificate !== void 0 && !/^[0-9a-f]{64}$/u.test(certificate)) {
|
|
261
|
+
throw new Error("Invalid Tileflow app identity");
|
|
262
|
+
}
|
|
263
|
+
return { id: value.id, certificateSha256: certificate };
|
|
264
|
+
}
|
|
224
265
|
function normalizeApiKey(value) {
|
|
225
266
|
if (typeof value !== "string" || value.length < 1 || value.length > 4096 || value !== value.trim() || /\p{Cc}/u.test(value)) {
|
|
226
267
|
throw new Error("Invalid Tileflow API key");
|
|
@@ -16,7 +16,10 @@ var geocodingLimits = Object.freeze({
|
|
|
16
16
|
maximumResponseBytes: 64 * 1024,
|
|
17
17
|
maximumSafeErrorBytes: 8 * 1024,
|
|
18
18
|
maximumSourceCharacters: 128,
|
|
19
|
-
maximumSuggestionTokenCharacters: 2048
|
|
19
|
+
maximumSuggestionTokenCharacters: 2048,
|
|
20
|
+
maximumDetailEntries: 100,
|
|
21
|
+
maximumOpeningHoursEntries: 20,
|
|
22
|
+
maximumOpeningHoursLines: 50
|
|
20
23
|
});
|
|
21
24
|
var boundedText = (maximum) => z.string().trim().min(1).max(maximum).refine((value) => !controlCharacterPattern.test(value), {
|
|
22
25
|
message: "Control characters are not allowed"
|
|
@@ -28,8 +31,14 @@ var geocodingRetentionModes = ["temporary"];
|
|
|
28
31
|
var geocodingBoundsSchema = z.tuple([longitude, latitude, longitude, latitude]).refine(([west, , east]) => west < east, { message: "West must be less than east" }).refine(([, south, , north]) => south < north, {
|
|
29
32
|
message: "South must be less than north"
|
|
30
33
|
});
|
|
34
|
+
var searchPlaceDetails = ["contacts", "openingHours", "timeZone", "accessPoints"];
|
|
35
|
+
var searchPlaceDetailSchema = z.enum(searchPlaceDetails);
|
|
36
|
+
var searchPlaceDetailsSchema = z.array(searchPlaceDetailSchema).min(1).max(searchPlaceDetails.length).refine((values) => new Set(values).size === values.length, {
|
|
37
|
+
message: "Place details must be unique"
|
|
38
|
+
});
|
|
31
39
|
var geocodingForwardRequestSchema = z.object({
|
|
32
40
|
bounds: geocodingBoundsSchema.optional(),
|
|
41
|
+
include: searchPlaceDetailsSchema.optional(),
|
|
33
42
|
language: z.string().max(geocodingLimits.maximumLanguageCharacters).regex(languagePattern).optional(),
|
|
34
43
|
limit: z.number().int().min(1).max(geocodingLimits.maximumLimit).optional(),
|
|
35
44
|
proximity: geocodingPositionSchema.optional(),
|
|
@@ -39,8 +48,10 @@ var geocodingForwardRequestSchema = z.object({
|
|
|
39
48
|
message: "Bounds and proximity are mutually exclusive",
|
|
40
49
|
path: ["proximity"]
|
|
41
50
|
});
|
|
51
|
+
var autocompleteIncludeSchema = z.tuple([z.literal("place")]);
|
|
42
52
|
var autocompleteRequestSchema = z.object({
|
|
43
53
|
bounds: geocodingBoundsSchema.optional(),
|
|
54
|
+
include: autocompleteIncludeSchema.optional(),
|
|
44
55
|
language: z.string().max(geocodingLimits.maximumLanguageCharacters).regex(languagePattern).optional(),
|
|
45
56
|
limit: z.number().int().min(1).max(geocodingLimits.maximumLimit).default(geocodingLimits.defaultLimit),
|
|
46
57
|
proximity: geocodingPositionSchema.optional(),
|
|
@@ -51,6 +62,7 @@ var autocompleteRequestSchema = z.object({
|
|
|
51
62
|
});
|
|
52
63
|
var reverseGeocodingKindSchema = z.enum(["address", "street", "locality", "place"]);
|
|
53
64
|
var geocodingReverseRequestSchema = z.object({
|
|
65
|
+
include: searchPlaceDetailsSchema.optional(),
|
|
54
66
|
kinds: z.array(reverseGeocodingKindSchema).min(1).max(4).optional(),
|
|
55
67
|
language: z.string().max(geocodingLimits.maximumLanguageCharacters).regex(languagePattern).optional(),
|
|
56
68
|
limit: z.number().int().min(1).max(geocodingLimits.maximumLimit).default(geocodingLimits.defaultReverseLimit),
|
|
@@ -93,10 +105,40 @@ var searchCategorySchema = z.object({
|
|
|
93
105
|
var searchPlaceMetadata = {
|
|
94
106
|
name: searchLiteralText(200).optional(),
|
|
95
107
|
categories: z.array(searchCategorySchema).min(1).max(100).optional(),
|
|
108
|
+
foodTypes: z.array(searchCategorySchema).min(1).max(100).optional(),
|
|
96
109
|
businessChains: z.array(z.object({ id: searchLiteralText(100), name: searchLiteralText(100) }).strict()).min(1).max(100).optional()
|
|
97
110
|
};
|
|
111
|
+
var contactListSchema = z.array(
|
|
112
|
+
z.object({
|
|
113
|
+
value: searchLiteralText(2048),
|
|
114
|
+
label: searchLiteralText(256).optional()
|
|
115
|
+
}).strict()
|
|
116
|
+
).min(1).max(geocodingLimits.maximumDetailEntries);
|
|
117
|
+
var searchContactsSchema = z.object({
|
|
118
|
+
phones: contactListSchema.optional(),
|
|
119
|
+
websites: contactListSchema.optional(),
|
|
120
|
+
emails: contactListSchema.optional()
|
|
121
|
+
}).strict();
|
|
122
|
+
var searchOpeningHoursSchema = z.array(
|
|
123
|
+
z.object({
|
|
124
|
+
display: z.array(searchLiteralText(256)).min(1).max(geocodingLimits.maximumOpeningHoursLines).optional(),
|
|
125
|
+
openNow: z.boolean().optional()
|
|
126
|
+
}).strict()
|
|
127
|
+
).min(1).max(geocodingLimits.maximumOpeningHoursEntries);
|
|
128
|
+
var searchTimeZoneSchema = z.object({
|
|
129
|
+
name: searchLiteralText(64),
|
|
130
|
+
utcOffsetSeconds: z.number().int().min(-86400).max(86400).optional()
|
|
131
|
+
}).strict();
|
|
132
|
+
var searchPlaceDetailFields = {
|
|
133
|
+
distanceMeters: z.number().finite().nonnegative().optional(),
|
|
134
|
+
contacts: searchContactsSchema.optional(),
|
|
135
|
+
openingHours: searchOpeningHoursSchema.optional(),
|
|
136
|
+
timeZone: searchTimeZoneSchema.optional(),
|
|
137
|
+
accessPoints: z.array(z.object({ position: geocodingPositionSchema }).strict()).min(1).max(geocodingLimits.maximumDetailEntries).optional()
|
|
138
|
+
};
|
|
98
139
|
var geocodingResultSchema = z.object({
|
|
99
140
|
...searchPlaceMetadata,
|
|
141
|
+
...searchPlaceDetailFields,
|
|
100
142
|
address: geocodingAddressSchema,
|
|
101
143
|
bounds: geocodingBoundsSchema.optional(),
|
|
102
144
|
kind: z.enum(geocodingResultKinds),
|
|
@@ -116,18 +158,21 @@ var geocodingAttributionSchema = z.object({
|
|
|
116
158
|
message: "Expected an HTTPS URL without credentials"
|
|
117
159
|
}).optional()
|
|
118
160
|
}).strict();
|
|
161
|
+
var placeOperationUnitsSchema = z.union([z.literal(10), z.literal(30)]);
|
|
119
162
|
var geocodingForwardResponseSchema = z.object({
|
|
120
163
|
attribution: z.array(geocodingAttributionSchema).min(1).max(geocodingLimits.maximumAttributionEntries),
|
|
121
164
|
queryId: z.string().regex(queryIdPattern),
|
|
122
165
|
results: z.array(geocodingResultSchema).max(geocodingLimits.maximumLimit),
|
|
123
166
|
schemaVersion: z.literal(1),
|
|
124
167
|
source: geocodingSourceSchema,
|
|
125
|
-
usage: z.object({ units:
|
|
168
|
+
usage: z.object({ units: placeOperationUnitsSchema }).strict()
|
|
126
169
|
}).strict();
|
|
127
170
|
var geocodingReverseResponseSchema = geocodingForwardResponseSchema;
|
|
128
171
|
var geocodingSuggestionSchema = z.object({
|
|
129
172
|
kind: z.enum(geocodingResultKinds),
|
|
130
173
|
label: boundedText(512),
|
|
174
|
+
addressLabel: boundedText(512).optional(),
|
|
175
|
+
place: geocodingResultSchema.optional(),
|
|
131
176
|
token: z.string().min(1).max(geocodingLimits.maximumSuggestionTokenCharacters).regex(/^[\x21-\x7e]+$/u)
|
|
132
177
|
}).strict();
|
|
133
178
|
var autocompleteResponseSchema = z.object({
|
|
@@ -135,9 +180,10 @@ var autocompleteResponseSchema = z.object({
|
|
|
135
180
|
schemaVersion: z.literal(1),
|
|
136
181
|
source: geocodingSourceSchema,
|
|
137
182
|
suggestions: z.array(geocodingSuggestionSchema).max(geocodingLimits.maximumLimit),
|
|
138
|
-
usage: z.object({ units: z.literal(10) }).strict()
|
|
183
|
+
usage: z.object({ units: z.union([z.literal(4), z.literal(10)]) }).strict()
|
|
139
184
|
}).strict();
|
|
140
185
|
var resolveSuggestionRequestSchema = z.object({
|
|
186
|
+
include: searchPlaceDetailsSchema.optional(),
|
|
141
187
|
retention: z.enum(geocodingRetentionModes).default("temporary"),
|
|
142
188
|
token: z.string().min(1).max(geocodingLimits.maximumSuggestionTokenCharacters).regex(/^[\x21-\x7e]+$/u)
|
|
143
189
|
}).strict();
|
|
@@ -146,7 +192,7 @@ var resolveSuggestionResponseSchema = z.object({
|
|
|
146
192
|
result: geocodingResultSchema,
|
|
147
193
|
schemaVersion: z.literal(1),
|
|
148
194
|
source: geocodingSourceSchema,
|
|
149
|
-
usage: z.object({ units:
|
|
195
|
+
usage: z.object({ units: placeOperationUnitsSchema }).strict()
|
|
150
196
|
}).strict();
|
|
151
197
|
function isCredentialFreeHttpsUrl(value) {
|
|
152
198
|
try {
|
|
@@ -177,12 +223,12 @@ var nearbyRequestSchema = z.object({
|
|
|
177
223
|
excludeCategories: categoryFilter.optional(),
|
|
178
224
|
countries: z.array(z.string().regex(/^(?:[A-Z]{2}|[A-Z]{3})$/u)).min(1).max(nearbyLimits.maximumCountries).optional(),
|
|
179
225
|
language: geocodingForwardRequestSchema.shape.language,
|
|
226
|
+
include: searchPlaceDetailsSchema.optional(),
|
|
180
227
|
limit: z.number().int().min(1).max(nearbyLimits.maximumLimit).default(nearbyLimits.defaultLimit),
|
|
181
228
|
cursor: nearbyCursorSchema.optional()
|
|
182
229
|
}).strict();
|
|
183
230
|
var nearbyPlaceSchema = geocodingResultSchema.omit({ sourceRef: true }).extend({
|
|
184
231
|
kind: z.literal("place"),
|
|
185
|
-
distanceMeters: z.number().finite().nonnegative().optional(),
|
|
186
232
|
token: geocodingSuggestionSchema.shape.token.optional()
|
|
187
233
|
}).strict();
|
|
188
234
|
var nearbyResponseSchema = z.object({
|
|
@@ -192,7 +238,7 @@ var nearbyResponseSchema = z.object({
|
|
|
192
238
|
nextCursor: nearbyCursorSchema.optional(),
|
|
193
239
|
source: geocodingSourceSchema,
|
|
194
240
|
attribution: geocodingForwardResponseSchema.shape.attribution,
|
|
195
|
-
usage: z.object({ units:
|
|
241
|
+
usage: z.object({ units: placeOperationUnitsSchema }).strict()
|
|
196
242
|
}).strict().refine(
|
|
197
243
|
(value) => new TextEncoder().encode(JSON.stringify(value)).byteLength <= nearbyLimits.maximumResponseBytes,
|
|
198
244
|
{ message: "Nearby response exceeds the encoded byte limit" }
|
|
@@ -207,13 +253,23 @@ var searchCategoriesResponseSchema = z.object({
|
|
|
207
253
|
}).strict();
|
|
208
254
|
var resolvePlaceRequestSchema = resolveSuggestionRequestSchema;
|
|
209
255
|
var resolvePlaceResponseSchema = resolveSuggestionResponseSchema;
|
|
256
|
+
var searchContactDetails = ["contacts", "openingHours"];
|
|
257
|
+
function searchOperationUnits(operation, include) {
|
|
258
|
+
if (operation === "categories") return 0;
|
|
259
|
+
if (operation === "autocomplete") return include?.length ? 10 : 4;
|
|
260
|
+
return include?.length ? 30 : 10;
|
|
261
|
+
}
|
|
210
262
|
|
|
211
263
|
export {
|
|
212
264
|
geocodingLimits,
|
|
213
265
|
geocodingPositionSchema,
|
|
214
266
|
geocodingRetentionModes,
|
|
215
267
|
geocodingBoundsSchema,
|
|
268
|
+
searchPlaceDetails,
|
|
269
|
+
searchPlaceDetailSchema,
|
|
270
|
+
searchPlaceDetailsSchema,
|
|
216
271
|
geocodingForwardRequestSchema,
|
|
272
|
+
autocompleteIncludeSchema,
|
|
217
273
|
autocompleteRequestSchema,
|
|
218
274
|
reverseGeocodingKindSchema,
|
|
219
275
|
geocodingReverseRequestSchema,
|
|
@@ -223,6 +279,10 @@ export {
|
|
|
223
279
|
searchCategoryIdSchema,
|
|
224
280
|
searchCategorySchema,
|
|
225
281
|
searchPlaceMetadata,
|
|
282
|
+
searchContactsSchema,
|
|
283
|
+
searchOpeningHoursSchema,
|
|
284
|
+
searchTimeZoneSchema,
|
|
285
|
+
searchPlaceDetailFields,
|
|
226
286
|
geocodingResultSchema,
|
|
227
287
|
geocodingSourceSchema,
|
|
228
288
|
geocodingAttributionSchema,
|
|
@@ -239,5 +299,7 @@ export {
|
|
|
239
299
|
nearbyResponseSchema,
|
|
240
300
|
searchCategoriesResponseSchema,
|
|
241
301
|
resolvePlaceRequestSchema,
|
|
242
|
-
resolvePlaceResponseSchema
|
|
302
|
+
resolvePlaceResponseSchema,
|
|
303
|
+
searchContactDetails,
|
|
304
|
+
searchOperationUnits
|
|
243
305
|
};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { AutocompleteRequest, AutocompleteResponse, GeocodingForwardRequest, GeocodingForwardResponse, GeocodingReverseRequest, GeocodingReverseResponse, SearchCategoriesResponse, ResolveSuggestionRequest, ResolveSuggestionResponse, NearbyRequest, NearbyResponse } from './contract.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
|
|
4
|
-
declare const GEOCODING_ERROR_CODES: readonly ["GEOCODING_FILTER_UNSUPPORTED", "GEOCODING_PLAN_UNAVAILABLE", "GEOCODING_EVALUATION_REQUIRED", "GEOCODING_EVALUATION_EXPIRED", "GEOCODING_EVALUATION_EXHAUSTED", "GEOCODING_INVALID_CURSOR", "GEOCODING_CURSOR_EXPIRED", "GEOCODING_ABORTED", "GEOCODING_DISABLED", "GEOCODING_INVALID_REQUEST", "GEOCODING_INVALID_SUGGESTION", "GEOCODING_PICK_LIMIT_EXCEEDED", "GEOCODING_QUOTA_EXCEEDED", "GEOCODING_REQUEST_TOO_LARGE", "GEOCODING_SUGGESTION_EXPIRED", "GEOCODING_UNAVAILABLE", "GEOCODING_UPSTREAM_INVALID", "GEOCODING_UPSTREAM_THROTTLED", "GEOCODING_UPSTREAM_TIMEOUT", "GEOCODING_UPSTREAM_UNAVAILABLE", "GEOCODING_USAGE_UNCONFIRMED"];
|
|
4
|
+
declare const GEOCODING_ERROR_CODES: readonly ["GEOCODING_FILTER_UNSUPPORTED", "GEOCODING_PLAN_UNAVAILABLE", "GEOCODING_EVALUATION_REQUIRED", "GEOCODING_EVALUATION_EXPIRED", "GEOCODING_EVALUATION_EXHAUSTED", "GEOCODING_INVALID_CURSOR", "GEOCODING_CURSOR_EXPIRED", "GEOCODING_ABORTED", "GEOCODING_DISABLED", "GEOCODING_INVALID_REQUEST", "GEOCODING_INVALID_SUGGESTION", "GEOCODING_ORIGIN_NOT_ALLOWED", "GEOCODING_PICK_LIMIT_EXCEEDED", "GEOCODING_QUOTA_EXCEEDED", "GEOCODING_REQUEST_TOO_LARGE", "GEOCODING_SPENDING_LIMIT_REACHED", "GEOCODING_CREDENTIAL_QUOTA_EXCEEDED", "GEOCODING_SUGGESTION_EXPIRED", "GEOCODING_SUSPENDED", "GEOCODING_UNAVAILABLE", "GEOCODING_UNPAID_BUDGET_EXHAUSTED", "GEOCODING_UNPAID_LIMIT_REACHED", "GEOCODING_UPSTREAM_INVALID", "GEOCODING_UPSTREAM_THROTTLED", "GEOCODING_UPSTREAM_TIMEOUT", "GEOCODING_UPSTREAM_UNAVAILABLE", "GEOCODING_USAGE_UNCONFIRMED"];
|
|
5
5
|
type GeocodingErrorCode = (typeof GEOCODING_ERROR_CODES)[number];
|
|
6
6
|
type GeocodeOptions = Readonly<{
|
|
7
7
|
apiKey: string;
|
|
8
8
|
apiUrl?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The native app a publishable credential admits: its iOS bundle ID, or its Android package name
|
|
11
|
+
* with the SHA-256 fingerprint of its signing certificate. Browsers send their origin instead.
|
|
12
|
+
*/
|
|
13
|
+
app?: Readonly<{
|
|
14
|
+
id: string;
|
|
15
|
+
certificateSha256?: string;
|
|
16
|
+
}>;
|
|
9
17
|
fetch?: typeof fetch;
|
|
10
18
|
signal?: AbortSignal;
|
|
11
19
|
}>;
|
package/dist/client.js
CHANGED