@forwardslashns/fws-geo-location-api 1.0.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.
- package/README.md +509 -0
- package/dist/client/geo-location.client.d.ts +121 -0
- package/dist/client/geo-location.client.d.ts.map +1 -0
- package/dist/client/geo-location.client.js +389 -0
- package/dist/constants/api.constants.d.ts +21 -0
- package/dist/constants/api.constants.d.ts.map +1 -0
- package/dist/constants/api.constants.js +28 -0
- package/dist/constants/continent.constants.d.ts +13 -0
- package/dist/constants/continent.constants.d.ts.map +1 -0
- package/dist/constants/continent.constants.js +21 -0
- package/dist/constants/feature.constants.d.ts +53 -0
- package/dist/constants/feature.constants.d.ts.map +1 -0
- package/dist/constants/feature.constants.js +55 -0
- package/dist/errors/geo-location.error.d.ts +6 -0
- package/dist/errors/geo-location.error.d.ts.map +1 -0
- package/dist/errors/geo-location.error.js +13 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/mappers/city.mapper.d.ts +4 -0
- package/dist/mappers/city.mapper.d.ts.map +1 -0
- package/dist/mappers/city.mapper.js +18 -0
- package/dist/mappers/country.mapper.d.ts +4 -0
- package/dist/mappers/country.mapper.d.ts.map +1 -0
- package/dist/mappers/country.mapper.js +19 -0
- package/dist/mappers/postal-code.mapper.d.ts +4 -0
- package/dist/mappers/postal-code.mapper.d.ts.map +1 -0
- package/dist/mappers/postal-code.mapper.js +24 -0
- package/dist/mappers/region.mapper.d.ts +4 -0
- package/dist/mappers/region.mapper.d.ts.map +1 -0
- package/dist/mappers/region.mapper.js +20 -0
- package/dist/services/http.service.d.ts +8 -0
- package/dist/services/http.service.d.ts.map +1 -0
- package/dist/services/http.service.js +60 -0
- package/dist/types/city.types.d.ts +21 -0
- package/dist/types/city.types.d.ts.map +1 -0
- package/dist/types/city.types.js +2 -0
- package/dist/types/client.types.d.ts +133 -0
- package/dist/types/client.types.d.ts.map +1 -0
- package/dist/types/client.types.js +2 -0
- package/dist/types/common.types.d.ts +29 -0
- package/dist/types/common.types.d.ts.map +1 -0
- package/dist/types/common.types.js +2 -0
- package/dist/types/country.types.d.ts +25 -0
- package/dist/types/country.types.d.ts.map +1 -0
- package/dist/types/country.types.js +2 -0
- package/dist/types/geonames-raw.types.d.ts +77 -0
- package/dist/types/geonames-raw.types.d.ts.map +1 -0
- package/dist/types/geonames-raw.types.js +2 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/postal-code.types.d.ts +31 -0
- package/dist/types/postal-code.types.d.ts.map +1 -0
- package/dist/types/postal-code.types.js +2 -0
- package/dist/types/region.types.d.ts +19 -0
- package/dist/types/region.types.d.ts.map +1 -0
- package/dist/types/region.types.js +2 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
# @forwardslashns/fws-geo-location-api
|
|
2
|
+
|
|
3
|
+
> **FWS company-dedicated TypeScript wrapper around the [GeoNames](https://www.geonames.org) geolocation service.**
|
|
4
|
+
|
|
5
|
+
Provides strongly-typed access to countries, administrative regions (states/provinces), cities, and postal codes — with automatic username rotation, global restriction support, paginated results, cursor-based chunked iteration, and frontend-friendly display names.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Prerequisites](#prerequisites)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [Constructor](#constructor)
|
|
15
|
+
- [Methods](#methods)
|
|
16
|
+
- [getCountries](#getcountries)
|
|
17
|
+
- [getRegions](#getregions)
|
|
18
|
+
- [getCities](#getcities)
|
|
19
|
+
- [getPostalCodes](#getpostalcodes)
|
|
20
|
+
- [getPostalCodesPage](#getpostalcodespage)
|
|
21
|
+
- [getAllPostalCodes](#getallpostalcodes)
|
|
22
|
+
- [findPostalCode](#findpostalcode)
|
|
23
|
+
- [Types](#types)
|
|
24
|
+
- [Constants](#constants)
|
|
25
|
+
- [Error Handling](#error-handling)
|
|
26
|
+
- [Global Restrictions](#global-restrictions)
|
|
27
|
+
- [Username Rotation](#username-rotation)
|
|
28
|
+
- [Publishing (FWS internal)](#publishing-fws-internal)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add @forwardslashns/fws-geo-location-api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Prerequisites
|
|
41
|
+
|
|
42
|
+
This library authenticates against the **GeoNames free API** using account usernames, not API keys.
|
|
43
|
+
|
|
44
|
+
1. Register one or more free accounts at https://www.geonames.org/login
|
|
45
|
+
2. Enable the free web services for each account in your profile settings
|
|
46
|
+
3. Pass the usernames in the `usernames` option when constructing the client
|
|
47
|
+
|
|
48
|
+
> The free tier allows ~20,000 credits per username per day. For high-volume applications register multiple service accounts and pass them all — the client will rotate automatically on rate-limit errors.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { GeoLocationClient } from '@forwardslashns/fws-geo-location-api';
|
|
56
|
+
|
|
57
|
+
const client = new GeoLocationClient({
|
|
58
|
+
usernames: ['myGeoNamesUser1', 'myGeoNamesUser2'],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// All countries
|
|
62
|
+
const countries = await client.getCountries();
|
|
63
|
+
|
|
64
|
+
// European countries only
|
|
65
|
+
const europe = await client.getCountries({ continents: ['EU'] });
|
|
66
|
+
|
|
67
|
+
// States / provinces for Germany
|
|
68
|
+
const regions = await client.getRegions('DE');
|
|
69
|
+
console.log(regions.data); // Region[]
|
|
70
|
+
|
|
71
|
+
// Cities in New York state
|
|
72
|
+
const cities = await client.getCities('US', { regionCode: 'NY' });
|
|
73
|
+
console.log(cities.data); // City[]
|
|
74
|
+
|
|
75
|
+
// Postal codes starting with "10" in the Netherlands
|
|
76
|
+
const postalCodes = await client.getPostalCodes('NL', { postalCodePrefix: '10' });
|
|
77
|
+
console.log(postalCodes.data); // PostalCode[]
|
|
78
|
+
|
|
79
|
+
// Look up a single postal code
|
|
80
|
+
const zip = await client.findPostalCode('10001', 'US');
|
|
81
|
+
console.log(zip?.displayName); // "10001 — New York City, New York, United States"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Constructor
|
|
87
|
+
|
|
88
|
+
### `new GeoLocationClient(options)`
|
|
89
|
+
|
|
90
|
+
| Option | Type | Required | Description |
|
|
91
|
+
| ------------------------- | ----------------------- | -------- | --------------------------------------------------------------------------------- |
|
|
92
|
+
| `usernames` | `[string, ...string[]]` | ✅ | One or more GeoNames usernames. TypeScript enforces at least one at compile time. |
|
|
93
|
+
| `restrictions.continents` | `ContinentCode[]` | — | Global continent filter applied to every call (can be overridden per call). |
|
|
94
|
+
| `restrictions.countries` | `string[]` | — | Global ISO 3166-1 alpha-2 country code allow-list applied to every call. |
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const client = new GeoLocationClient({
|
|
98
|
+
usernames: ['user1', 'user2'],
|
|
99
|
+
restrictions: {
|
|
100
|
+
continents: ['EU'],
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Methods
|
|
108
|
+
|
|
109
|
+
### `getCountries`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
getCountries(options?: GetCountriesOptions): Promise<Country[]>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Returns all countries (or a filtered subset). Not paginated — the GeoNames country list is small (~250 entries).
|
|
116
|
+
|
|
117
|
+
| Option | Type | Description |
|
|
118
|
+
| -------------- | ----------------- | ------------------------------------------------------------- |
|
|
119
|
+
| `continents` | `ContinentCode[]` | Filter to specific continents (overrides global restriction). |
|
|
120
|
+
| `countryCodes` | `string[]` | Filter to specific ISO2 codes (overrides global restriction). |
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// All countries
|
|
124
|
+
const all = await client.getCountries();
|
|
125
|
+
|
|
126
|
+
// Only DACH
|
|
127
|
+
const dach = await client.getCountries({ countryCodes: ['DE', 'AT', 'CH'] });
|
|
128
|
+
|
|
129
|
+
// Only Europe
|
|
130
|
+
const eu = await client.getCountries({ continents: ['EU'] });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**`Country` fields:**
|
|
134
|
+
|
|
135
|
+
| Field | Type | Description |
|
|
136
|
+
| --------------- | ---------- | ------------------------------------------- |
|
|
137
|
+
| `countryCode` | `string` | ISO 3166-1 alpha-2, e.g. `"DE"` |
|
|
138
|
+
| `countryName` | `string` | English name, e.g. `"Germany"` |
|
|
139
|
+
| `continent` | `string` | Two-letter continent code, e.g. `"EU"` |
|
|
140
|
+
| `continentName` | `string` | Continent full name, e.g. `"Europe"` |
|
|
141
|
+
| `capital` | `string` | Capital city name |
|
|
142
|
+
| `currencyCode` | `string` | ISO 4217 currency code, e.g. `"EUR"` |
|
|
143
|
+
| `currencyName` | `string` | Currency display name, e.g. `"Euro"` |
|
|
144
|
+
| `population` | `number` | Total country population |
|
|
145
|
+
| `languages` | `string[]` | BCP 47 language codes spoken in the country |
|
|
146
|
+
| `areaInSqKm` | `number` | Land area in square kilometres |
|
|
147
|
+
| `geonameId` | `number` | GeoNames unique identifier |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `getRegions`
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
getRegions(countryCode: string, options?: GetRegionsOptions): Promise<PaginatedResult<Region>>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Returns first-order administrative divisions (states, provinces, prefectures, etc.) for a country.
|
|
158
|
+
|
|
159
|
+
| Option | Type | Description |
|
|
160
|
+
| --------- | -------- | -------------------------------------------------- |
|
|
161
|
+
| `maxRows` | `number` | Max results (GeoNames cap: 1 000). Default: 1 000. |
|
|
162
|
+
| `page` | `number` | 1-based page number. Default: 1. |
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const result = await client.getRegions('US');
|
|
166
|
+
console.log(result.data); // Region[]
|
|
167
|
+
console.log(result.total); // total count
|
|
168
|
+
console.log(result.hasMore); // boolean
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**`Region` fields:**
|
|
172
|
+
|
|
173
|
+
| Field | Type | Description |
|
|
174
|
+
| ------------- | -------- | ------------------------------------ |
|
|
175
|
+
| `geonameId` | `number` | GeoNames unique identifier |
|
|
176
|
+
| `name` | `string` | Region name, e.g. `"New York"` |
|
|
177
|
+
| `code` | `string` | Short admin code, e.g. `"NY"` |
|
|
178
|
+
| `isoCode` | `string` | Full ISO code, e.g. `"US-NY"` |
|
|
179
|
+
| `countryCode` | `string` | Parent country ISO2 code |
|
|
180
|
+
| `population` | `number` | Population (0 if unknown) |
|
|
181
|
+
| `lat` | `number` | Latitude of centroid |
|
|
182
|
+
| `lng` | `number` | Longitude of centroid |
|
|
183
|
+
| `featureCode` | `string` | GeoNames feature code, e.g. `"ADM1"` |
|
|
184
|
+
| `adminCode1` | `string` | Raw admin level-1 code from GeoNames |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### `getCities`
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
getCities(countryCode: string | null, options?: GetCitiesOptions): Promise<PaginatedResult<City>>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Returns populated places. Pass `null` for `countryCode` to search globally.
|
|
195
|
+
|
|
196
|
+
| Option | Type | Description |
|
|
197
|
+
| ------------ | -------------------------------------------- | -------------------------------------------------- |
|
|
198
|
+
| `regionCode` | `string` | Filter by admin level-1 code (state/province). |
|
|
199
|
+
| `searchTerm` | `string` | Filter by place name prefix. |
|
|
200
|
+
| `maxRows` | `number` | Max results (GeoNames cap: 1 000). Default: 1 000. |
|
|
201
|
+
| `orderBy` | `'population' \| 'elevation' \| 'relevance'` | Sort order. Default: `'population'`. |
|
|
202
|
+
| `page` | `number` | 1-based page number. Default: 1. |
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// 100 most populous Japanese cities
|
|
206
|
+
const result = await client.getCities('JP', { maxRows: 100 });
|
|
207
|
+
|
|
208
|
+
// Global search — find all cities named "Paris"
|
|
209
|
+
const paris = await client.getCities(null, { searchTerm: 'Paris' });
|
|
210
|
+
|
|
211
|
+
// New York state cities
|
|
212
|
+
const nyc = await client.getCities('US', { regionCode: 'NY' });
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**`City` fields:**
|
|
216
|
+
|
|
217
|
+
| Field | Type | Description |
|
|
218
|
+
| ------------- | -------- | ----------------------------------- |
|
|
219
|
+
| `geonameId` | `number` | GeoNames unique identifier |
|
|
220
|
+
| `name` | `string` | City name |
|
|
221
|
+
| `countryCode` | `string` | ISO2 country code |
|
|
222
|
+
| `adminName1` | `string` | Region / state name |
|
|
223
|
+
| `adminCode1` | `string` | Region short code |
|
|
224
|
+
| `population` | `number` | Population |
|
|
225
|
+
| `lat` | `number` | Latitude |
|
|
226
|
+
| `lng` | `number` | Longitude |
|
|
227
|
+
| `featureCode` | `string` | GeoNames feature code, e.g. `"PPL"` |
|
|
228
|
+
| `featureName` | `string` | Human-readable feature type |
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### `getPostalCodes`
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
getPostalCodes(countryCode: string, options?: GetPostalCodesOptions): Promise<PaginatedResult<PostalCode>>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Returns postal codes matching the supplied filter. **A filter is required for large countries** (US, DE, etc.) due to a GeoNames dataset-size limit — callers that omit a filter for an oversized country receive a descriptive `GeoLocationError` pointing them to `getAllPostalCodes` or `getPostalCodesPage`.
|
|
239
|
+
|
|
240
|
+
| Option | Type | Description |
|
|
241
|
+
| ------------------ | -------- | ------------------------------------------------------- |
|
|
242
|
+
| `postalCodePrefix` | `string` | Return codes whose postal code starts with this string. |
|
|
243
|
+
| `placeName` | `string` | Return codes whose place name starts with this string. |
|
|
244
|
+
| `maxRows` | `number` | Max results (GeoNames cap: 1 000). Default: 100. |
|
|
245
|
+
| `page` | `number` | 1-based page number. Default: 1. |
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
// ZIP codes in the Netherlands starting with "10"
|
|
249
|
+
const nld = await client.getPostalCodes('NL', { postalCodePrefix: '10' });
|
|
250
|
+
|
|
251
|
+
// US codes filtered by city name
|
|
252
|
+
const nyZips = await client.getPostalCodes('US', { placeName: 'New York', maxRows: 20 });
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Each result has a `displayName` formatted for dropdowns:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
"10001 — New York City, New York, United States"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### `getPostalCodesPage`
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
getPostalCodesPage(countryCode: string, options?: GetPostalCodePageOptions): Promise<PostalCodePage>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Cursor-based paginated iteration over the **entire** postal code dataset of a country, including large countries like the US. Internally iterates 36 alphanumeric prefix buckets (0–9, A–Z) so every call works regardless of country size.
|
|
270
|
+
|
|
271
|
+
Typical cost: **1 GeoNames API request per call** (occasionally 2 at a prefix bucket boundary).
|
|
272
|
+
|
|
273
|
+
| Option | Type | Description |
|
|
274
|
+
| --------- | -------- | -------------------------------------------------------------------------------------- |
|
|
275
|
+
| `cursor` | `string` | Opaque cursor returned by the previous call. Omit to start from the first postal code. |
|
|
276
|
+
| `maxRows` | `number` | Records per page (capped at 500). Default: 100. |
|
|
277
|
+
|
|
278
|
+
**Returns `PostalCodePage`:**
|
|
279
|
+
|
|
280
|
+
| Field | Type | Description |
|
|
281
|
+
| ---------------- | ----------------------------- | ------------------------------------------------------------------ |
|
|
282
|
+
| `data` | `PostalCode[]` | Postal codes for this page. |
|
|
283
|
+
| `nextCursor` | `string \| null` | Pass to the next call to get the following page. `null` when done. |
|
|
284
|
+
| `hasMore` | `boolean` | Convenience alias: `true` when `nextCursor` is not `null`. |
|
|
285
|
+
| `prefixProgress` | `{ done: number; total: 36 }` | How many of the 36 prefix buckets have been fully processed. |
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Iterate all US postal codes in pages of 200
|
|
289
|
+
let page = await client.getPostalCodesPage('US', { maxRows: 200 });
|
|
290
|
+
|
|
291
|
+
while (true) {
|
|
292
|
+
for (const zip of page.data) {
|
|
293
|
+
console.log(zip.postalCode, zip.placeName);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!page.hasMore) break;
|
|
297
|
+
page = await client.getPostalCodesPage('US', {
|
|
298
|
+
cursor: page.nextCursor!,
|
|
299
|
+
maxRows: 200,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
> **Tip:** Store the cursor externally to resume across sessions or HTTP requests.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### `getAllPostalCodes`
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
getAllPostalCodes(countryCode: string, options?: GetAllPostalCodesOptions): Promise<PostalCode[]>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Fetches every postal code for a country in a single call by iterating all 36 prefix buckets internally. Results are deduplicated and returned sorted by postal code. For the US (~43 000 codes) this makes ~80 GeoNames requests.
|
|
315
|
+
|
|
316
|
+
| Option | Type | Description |
|
|
317
|
+
| ------------ | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
318
|
+
| `onProgress` | `(loaded: ReadonlyArray<PostalCode>) => void` | Called after each prefix bucket is fetched. Use to stream progress into your UI. |
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
const allCodes = await client.getAllPostalCodes('DE', {
|
|
322
|
+
onProgress: (loaded) => {
|
|
323
|
+
console.log(`${loaded.length} codes loaded so far…`);
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
console.log(`Total: ${allCodes.length}`);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
> **Use this method** when you need the complete dataset in memory (e.g. to build a local search index). Use `getPostalCodesPage` when you only need a few pages at a time.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### `findPostalCode`
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
findPostalCode(postalCode: string, countryCode: string): Promise<PostalCode | null>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Looks up a single postal code by its exact string. Returns `null` when not found. Uses exactly 1 GeoNames API credit.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
const result = await client.findPostalCode('SW1A 1AA', 'GB');
|
|
344
|
+
if (result) {
|
|
345
|
+
console.log(result.displayName);
|
|
346
|
+
// "SW1A 1AA — Westminster, England, United Kingdom"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const missing = await client.findPostalCode('00000', 'US');
|
|
350
|
+
console.log(missing); // null
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Types
|
|
356
|
+
|
|
357
|
+
### `PaginatedResult<T>`
|
|
358
|
+
|
|
359
|
+
Returned by `getRegions`, `getCities`, and `getPostalCodes`.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
interface PaginatedResult<T> {
|
|
363
|
+
data: T[];
|
|
364
|
+
total: number; // -1 when GeoNames does not return an exact count
|
|
365
|
+
page: number; // 1-based
|
|
366
|
+
pageSize: number;
|
|
367
|
+
hasMore: boolean;
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### `PostalCode`
|
|
372
|
+
|
|
373
|
+
```typescript
|
|
374
|
+
interface PostalCode {
|
|
375
|
+
postalCode: string;
|
|
376
|
+
placeName: string;
|
|
377
|
+
countryCode: string;
|
|
378
|
+
countryName: string;
|
|
379
|
+
adminName1: string; // state / region
|
|
380
|
+
adminCode1: string;
|
|
381
|
+
adminName2: string; // district / county
|
|
382
|
+
adminCode2: string;
|
|
383
|
+
lat: number;
|
|
384
|
+
lng: number;
|
|
385
|
+
/** Pre-formatted label: "10001 — New York City, New York, United States" */
|
|
386
|
+
displayName: string;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### `PostalCodePage`
|
|
391
|
+
|
|
392
|
+
Returned by `getPostalCodesPage`.
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
interface PostalCodePage {
|
|
396
|
+
data: PostalCode[];
|
|
397
|
+
nextCursor: string | null;
|
|
398
|
+
hasMore: boolean;
|
|
399
|
+
prefixProgress: { done: number; total: 36 };
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### `AtLeastOne<T>`
|
|
404
|
+
|
|
405
|
+
Utility type enforcing a non-empty array at compile time.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
type AtLeastOne<T> = [T, ...T[]];
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Constants
|
|
414
|
+
|
|
415
|
+
### Continent codes
|
|
416
|
+
|
|
417
|
+
```typescript
|
|
418
|
+
import { CONTINENT_CODES, CONTINENT_NAMES } from '@forwardslashns/fws-geo-location-api';
|
|
419
|
+
|
|
420
|
+
CONTINENT_CODES.AFRICA; // 'AF'
|
|
421
|
+
CONTINENT_CODES.ANTARCTICA; // 'AN'
|
|
422
|
+
CONTINENT_CODES.ASIA; // 'AS'
|
|
423
|
+
CONTINENT_CODES.EUROPE; // 'EU'
|
|
424
|
+
CONTINENT_CODES.NORTH_AMERICA; // 'NA'
|
|
425
|
+
CONTINENT_CODES.OCEANIA; // 'OC'
|
|
426
|
+
CONTINENT_CODES.SOUTH_AMERICA; // 'SA'
|
|
427
|
+
|
|
428
|
+
CONTINENT_NAMES['EU']; // 'Europe'
|
|
429
|
+
CONTINENT_NAMES['NA']; // 'North America'
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Error Handling
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { GeoLocationClient, GeoLocationError } from '@forwardslashns/fws-geo-location-api';
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const countries = await client.getCountries();
|
|
441
|
+
} catch (err) {
|
|
442
|
+
if (err instanceof GeoLocationError) {
|
|
443
|
+
console.error(err.message); // human-readable description
|
|
444
|
+
console.error(err.code); // GeoNames numeric status code, if applicable
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Common error scenarios
|
|
450
|
+
|
|
451
|
+
| Scenario | Behaviour |
|
|
452
|
+
| ------------------------------------------------ | ------------------------------------------------------------------------------------ |
|
|
453
|
+
| `getPostalCodes` without filter on large country | Throws `GeoLocationError` explaining the limit and recommending `getPostalCodesPage` |
|
|
454
|
+
| All usernames exhausted (rate-limited) | Throws `GeoLocationError` asking to add usernames or wait for credit reset |
|
|
455
|
+
| Invalid / empty `countryCode` | Throws `GeoLocationError` immediately (no network call made) |
|
|
456
|
+
| GeoNames returns status for no results | Returns empty `PaginatedResult` (not an error) |
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Global Restrictions
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
const client = new GeoLocationClient({
|
|
464
|
+
usernames: ['myUser'],
|
|
465
|
+
restrictions: {
|
|
466
|
+
continents: ['EU'], // only European data
|
|
467
|
+
countries: ['DE', 'AT', 'CH'], // only DACH countries
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// All calls are automatically filtered by the restrictions
|
|
472
|
+
const countries = await client.getCountries(); // returns only DE, AT, CH
|
|
473
|
+
const cities = await client.getCities('FR'); // returns nothing — FR not in allow-list
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Username Rotation
|
|
479
|
+
|
|
480
|
+
When a username exceeds its daily/hourly/weekly credit limit (GeoNames status codes 17–20) the client automatically advances to the next username in the array. The current call is transparently retried with the new username.
|
|
481
|
+
|
|
482
|
+
If **all** usernames are exhausted a `GeoLocationError` is thrown with a clear message.
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
const client = new GeoLocationClient({
|
|
486
|
+
// Rotate across five accounts for high-volume usage
|
|
487
|
+
usernames: ['account1', 'account2', 'account3', 'account4', 'account5'],
|
|
488
|
+
});
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Publishing (FWS internal)
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
# Create .env with your npm token
|
|
497
|
+
echo "NPM_TOKEN=your_token_here" > .env
|
|
498
|
+
|
|
499
|
+
# Bump patch version, build, and publish
|
|
500
|
+
node ./publish.js
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Published to the public npm registry under the `@forwardslashns` scope.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## License
|
|
508
|
+
|
|
509
|
+
ISC — ForwardSlash d.o.o.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { GeoLocationClientOptions, GetCountriesOptions, GetRegionsOptions, GetCitiesOptions, GetPostalCodesOptions, GetAllPostalCodesOptions, GetPostalCodePageOptions, PostalCodePage } from '../types/client.types.js';
|
|
2
|
+
import type { PaginatedResult } from '../types/common.types.js';
|
|
3
|
+
import type { Country } from '../types/country.types.js';
|
|
4
|
+
import type { Region } from '../types/region.types.js';
|
|
5
|
+
import type { City } from '../types/city.types.js';
|
|
6
|
+
import type { PostalCode } from '../types/postal-code.types.js';
|
|
7
|
+
export declare class GeoLocationClient {
|
|
8
|
+
private readonly httpService;
|
|
9
|
+
private readonly restrictions;
|
|
10
|
+
/** In-memory cache: ISO2 countryCode → Country. Populated on first full country fetch. */
|
|
11
|
+
private readonly countryCache;
|
|
12
|
+
constructor(options: GeoLocationClientOptions);
|
|
13
|
+
/**
|
|
14
|
+
* Returns a list of countries.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Optional per-call overrides for continent and country code filters.
|
|
17
|
+
* When omitted the global `restrictions` passed to the constructor are used.
|
|
18
|
+
*/
|
|
19
|
+
getCountries(options?: GetCountriesOptions): Promise<Country[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Returns first-order administrative divisions (states, provinces, etc.)
|
|
22
|
+
* for the given country.
|
|
23
|
+
*
|
|
24
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"US"`.
|
|
25
|
+
* @param options - Optional per-call options.
|
|
26
|
+
*/
|
|
27
|
+
getRegions(countryCode: string, options?: GetRegionsOptions): Promise<PaginatedResult<Region>>;
|
|
28
|
+
/**
|
|
29
|
+
* Returns populated places (cities, towns) for the given country,
|
|
30
|
+
* ordered by population descending.
|
|
31
|
+
*
|
|
32
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"DE"`.
|
|
33
|
+
* @param options - Optional filters: `regionCode`, `maxRows`, `orderBy`.
|
|
34
|
+
*/
|
|
35
|
+
getCities(countryCode?: string | null, options?: GetCitiesOptions): Promise<PaginatedResult<City>>;
|
|
36
|
+
/**
|
|
37
|
+
* Returns postal / zip codes for the given country. Supports optional
|
|
38
|
+
* prefix and place-name filters.
|
|
39
|
+
*
|
|
40
|
+
* Each result includes a `displayName` formatted as:
|
|
41
|
+
* `"{postalCode} — {placeName}, {adminName1}, {countryName}"`
|
|
42
|
+
*
|
|
43
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"US"`.
|
|
44
|
+
* @param options - Optional filters: `postalCodePrefix`, `placeName`, `maxRows`, `startRow`, `page`.
|
|
45
|
+
*
|
|
46
|
+
* **GeoNames limitation — a filter is required for large countries.**
|
|
47
|
+
* The underlying GeoNames `postalCodeSearchJSON` endpoint rejects bare country-only
|
|
48
|
+
* queries for countries with large postal code datasets (e.g. US, DE, FR) by returning
|
|
49
|
+
* status 15 ("no result found"). Providing either `postalCodePrefix` or `placeName`
|
|
50
|
+
* always works regardless of country size.
|
|
51
|
+
*
|
|
52
|
+
* To retrieve **all** postal codes for a country without knowing a prefix in advance,
|
|
53
|
+
* use {@link getAllPostalCodes} instead — it handles this constraint transparently by
|
|
54
|
+
* iterating through alphanumeric prefix buckets and supports a progressive `onProgress`
|
|
55
|
+
* callback for streaming results into the UI.
|
|
56
|
+
*/
|
|
57
|
+
getPostalCodes(countryCode: string, options?: GetPostalCodesOptions): Promise<PaginatedResult<PostalCode>>;
|
|
58
|
+
/**
|
|
59
|
+
* Returns **all** postal codes for the given country by iterating through
|
|
60
|
+
* alphanumeric prefix buckets (`0–9`, `A–Z`) and paginating each bucket.
|
|
61
|
+
*
|
|
62
|
+
* Because GeoNames requires at least one search term, a bare country-only
|
|
63
|
+
* query always returns status 15. This method works around that constraint
|
|
64
|
+
* transparently: it fires one request per character prefix (up to 36), each
|
|
65
|
+
* with `postalcode_startsWith=<char>`, and pages through results in chunks
|
|
66
|
+
* of 1 000 until the bucket is exhausted. Prefixes that yield no results
|
|
67
|
+
* (status 15 or empty page) are skipped automatically.
|
|
68
|
+
*
|
|
69
|
+
* The results are deduplicated and returned as a single flat array sorted by
|
|
70
|
+
* postal code.
|
|
71
|
+
*
|
|
72
|
+
* **API credit cost**: roughly 1 request per 1 000 postal codes in the
|
|
73
|
+
* country, spread across up to 36 prefix buckets. For small countries
|
|
74
|
+
* (< 5 000 codes) expect 10–20 requests; for large ones (US ~43 000) expect
|
|
75
|
+
* ~80 requests.
|
|
76
|
+
*
|
|
77
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"US"`.
|
|
78
|
+
*/
|
|
79
|
+
getAllPostalCodes(countryCode: string, options?: GetAllPostalCodesOptions): Promise<PostalCode[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Returns one page of postal codes for a country, working around the GeoNames
|
|
82
|
+
* per-country query limit that prevents retrieving all codes for large
|
|
83
|
+
* countries (e.g. US) in a single request.
|
|
84
|
+
*
|
|
85
|
+
* Internally the library iterates 36 alphanumeric prefix buckets (0–9, A–Z)
|
|
86
|
+
* and stores progress as an opaque cursor so callers never need to track
|
|
87
|
+
* offsets themselves.
|
|
88
|
+
*
|
|
89
|
+
* **Typical usage**
|
|
90
|
+
* ```ts
|
|
91
|
+
* let page = await client.getPostalCodesPage('US', { maxRows: 200 });
|
|
92
|
+
* while (page.hasMore) {
|
|
93
|
+
* // process page.data …
|
|
94
|
+
* page = await client.getPostalCodesPage('US', { cursor: page.nextCursor! });
|
|
95
|
+
* }
|
|
96
|
+
* // process last page.data …
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* **API credit cost**: 1 GeoNames request per call (2 at the boundary
|
|
100
|
+
* between two prefix buckets). Far cheaper than `getAllPostalCodes` when
|
|
101
|
+
* you only need the first few pages.
|
|
102
|
+
*
|
|
103
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"US"`.
|
|
104
|
+
* @param options - Optional cursor and page-size settings.
|
|
105
|
+
*/
|
|
106
|
+
getPostalCodesPage(countryCode: string, options?: GetPostalCodePageOptions): Promise<PostalCodePage>;
|
|
107
|
+
/**
|
|
108
|
+
* Finds a single postal code by its exact code string within a given country.
|
|
109
|
+
* Returns `null` when not found.
|
|
110
|
+
*
|
|
111
|
+
* @param postalCode - The exact postal / zip code to look up, e.g. `"10001"`.
|
|
112
|
+
* @param countryCode - ISO 3166-1 alpha-2 country code, e.g. `"US"`.
|
|
113
|
+
*/
|
|
114
|
+
findPostalCode(postalCode: string, countryCode: string): Promise<PostalCode | null>;
|
|
115
|
+
private fetchAllCountries;
|
|
116
|
+
private fetchCountriesByContinent;
|
|
117
|
+
private cacheCountries;
|
|
118
|
+
private resolveCountryName;
|
|
119
|
+
private assertNonEmptyCountryCode;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=geo-location.client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo-location.client.d.ts","sourceRoot":"","sources":["../../src/client/geo-location.client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EAExB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,wBAAwB,EACxB,wBAAwB,EACxB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAuBhE,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,0FAA0F;IAC1F,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAmC;gBAE7C,OAAO,EAAE,wBAAwB;IASpD;;;;;OAKG;IACU,YAAY,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAyB5E;;;;;;OAMG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IAsC3G;;;;;;OAMG;IACU,SAAS,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IA+C/G;;;;;;;;;;;;;;;;;;;;OAoBG;IACU,cAAc,CACzB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC;IAuDvC;;;;;;;;;;;;;;;;;;;;OAoBG;IACU,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAoD9G;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACU,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC;IAmEjH;;;;;;OAMG;IACU,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;YA8BlF,iBAAiB;YAUjB,yBAAyB;IAUvC,OAAO,CAAC,cAAc;YAMR,kBAAkB;IAUhC,OAAO,CAAC,yBAAyB;CAKlC"}
|