csv2geo-sdk 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 +208 -0
- package/package.json +40 -0
- package/src/errors.js +75 -0
- package/src/index.d.ts +126 -0
- package/src/index.js +296 -0
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# CSV2GEO Node.js SDK
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/csv2geo-sdk)
|
|
4
|
+
[](https://www.npmjs.com/package/csv2geo-sdk)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Official Node.js SDK for the [CSV2GEO Geocoding API](https://csv2geo.com) - fast, accurate geocoding powered by 446M+ addresses worldwide.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install csv2geo-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { Client } = require('csv2geo-sdk');
|
|
19
|
+
|
|
20
|
+
// Initialize with your API key
|
|
21
|
+
const client = new Client('your_api_key');
|
|
22
|
+
|
|
23
|
+
// Forward geocoding (address → coordinates)
|
|
24
|
+
const result = await client.geocode('1600 Pennsylvania Ave, Washington DC');
|
|
25
|
+
if (result) {
|
|
26
|
+
console.log(`Lat: ${result.lat}, Lng: ${result.lng}`);
|
|
27
|
+
console.log(`Address: ${result.formattedAddress}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Reverse geocoding (coordinates → address)
|
|
31
|
+
const result = await client.reverse(38.8977, -77.0365);
|
|
32
|
+
if (result) {
|
|
33
|
+
console.log(`Address: ${result.formattedAddress}`);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Forward geocoding** - Convert addresses to coordinates
|
|
40
|
+
- **Reverse geocoding** - Convert coordinates to addresses
|
|
41
|
+
- **Batch processing** - Geocode up to 10,000 addresses per request
|
|
42
|
+
- **Auto-retry** - Automatic retry on rate limits
|
|
43
|
+
- **TypeScript support** - Full type definitions included
|
|
44
|
+
- **Zero dependencies** - Uses native `fetch` (Node.js 18+)
|
|
45
|
+
|
|
46
|
+
## API Reference
|
|
47
|
+
|
|
48
|
+
### Initialize Client
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const { Client } = require('csv2geo-sdk');
|
|
52
|
+
|
|
53
|
+
const client = new Client('your_api_key', {
|
|
54
|
+
baseUrl: 'https://api.csv2geo.com/v1', // optional
|
|
55
|
+
timeout: 30000, // optional, milliseconds
|
|
56
|
+
autoRetry: true, // optional, retry on rate limit
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Forward Geocoding
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// Simple - returns best match or null
|
|
64
|
+
const result = await client.geocode('1600 Pennsylvania Ave, Washington DC');
|
|
65
|
+
|
|
66
|
+
// With country filter
|
|
67
|
+
const result = await client.geocode('123 Main St', { country: 'US' });
|
|
68
|
+
|
|
69
|
+
// Full response with all matches
|
|
70
|
+
const response = await client.geocodeFull('1600 Pennsylvania Ave');
|
|
71
|
+
for (const result of response.results) {
|
|
72
|
+
console.log(`${result.formattedAddress}: ${result.accuracyScore}`);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Reverse Geocoding
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
// Simple - returns best match or null
|
|
80
|
+
const result = await client.reverse(38.8977, -77.0365);
|
|
81
|
+
|
|
82
|
+
// Full response with all matches
|
|
83
|
+
const response = await client.reverseFull(38.8977, -77.0365);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Batch Geocoding
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Geocode multiple addresses (up to 10,000)
|
|
90
|
+
const addresses = [
|
|
91
|
+
'1600 Pennsylvania Ave, Washington DC',
|
|
92
|
+
'350 Fifth Avenue, New York, NY',
|
|
93
|
+
'1 Infinite Loop, Cupertino, CA',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const results = await client.geocodeBatch(addresses);
|
|
97
|
+
for (const response of results) {
|
|
98
|
+
const best = response.results[0];
|
|
99
|
+
if (best) {
|
|
100
|
+
console.log(`${response.query}: ${best.lat}, ${best.lng}`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log(`${response.query}: Not found`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Batch Reverse Geocoding
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Reverse geocode multiple coordinates
|
|
111
|
+
const coordinates = [
|
|
112
|
+
{ lat: 38.8977, lng: -77.0365 },
|
|
113
|
+
{ lat: 40.7484, lng: -73.9857 },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const results = await client.reverseBatch(coordinates);
|
|
117
|
+
for (const response of results) {
|
|
118
|
+
const best = response.results[0];
|
|
119
|
+
if (best) {
|
|
120
|
+
console.log(best.formattedAddress);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### GeocodeResult Object
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
const result = await client.geocode('1600 Pennsylvania Ave, Washington DC');
|
|
129
|
+
|
|
130
|
+
// Coordinates
|
|
131
|
+
result.lat // 38.8977
|
|
132
|
+
result.lng // -77.0365
|
|
133
|
+
|
|
134
|
+
// Address
|
|
135
|
+
result.formattedAddress // "1600 PENNSYLVANIA AVE NW, WASHINGTON, DC 20500, US"
|
|
136
|
+
result.accuracy // "rooftop"
|
|
137
|
+
result.accuracyScore // 1.0
|
|
138
|
+
|
|
139
|
+
// Components
|
|
140
|
+
result.components.houseNumber // "1600"
|
|
141
|
+
result.components.street // "PENNSYLVANIA AVE NW"
|
|
142
|
+
result.components.city // "WASHINGTON"
|
|
143
|
+
result.components.state // "DC"
|
|
144
|
+
result.components.postcode // "20500"
|
|
145
|
+
result.components.country // "US"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Error Handling
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
const { Client, AuthenticationError, RateLimitError, InvalidRequestError } = require('csv2geo-sdk');
|
|
152
|
+
|
|
153
|
+
const client = new Client('your_api_key');
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await client.geocode('123 Main St');
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof AuthenticationError) {
|
|
159
|
+
console.log(`Invalid API key: ${err.message}`);
|
|
160
|
+
} else if (err instanceof RateLimitError) {
|
|
161
|
+
console.log(`Rate limited. Retry after ${err.retryAfter} seconds`);
|
|
162
|
+
} else if (err instanceof InvalidRequestError) {
|
|
163
|
+
console.log(`Invalid request: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Rate Limits
|
|
169
|
+
|
|
170
|
+
The client tracks rate limit headers automatically:
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
await client.geocode('123 Main St');
|
|
174
|
+
|
|
175
|
+
console.log(client.rateLimit); // Max requests per minute
|
|
176
|
+
console.log(client.rateLimitRemaining); // Requests remaining
|
|
177
|
+
console.log(client.rateLimitReset); // Unix timestamp when limit resets
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
With `autoRetry: true` (default), the client automatically waits and retries when rate limited.
|
|
181
|
+
|
|
182
|
+
## TypeScript
|
|
183
|
+
|
|
184
|
+
Full TypeScript support is included:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { Client, GeocodeResult, GeocodeResponse } from 'csv2geo-sdk';
|
|
188
|
+
|
|
189
|
+
const client = new Client('your_api_key');
|
|
190
|
+
const result: GeocodeResult | null = await client.geocode('123 Main St');
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Requirements
|
|
194
|
+
|
|
195
|
+
- Node.js 16+ (uses native `fetch`)
|
|
196
|
+
|
|
197
|
+
## Get Your API Key
|
|
198
|
+
|
|
199
|
+
Sign up at [csv2geo.com](https://csv2geo.com) to get your API key.
|
|
200
|
+
|
|
201
|
+
## Documentation
|
|
202
|
+
|
|
203
|
+
- [API Documentation](https://acenji.github.io/csv2geo-api/docs/)
|
|
204
|
+
- [OpenAPI Specification](https://github.com/acenji/csv2geo-api/blob/main/openapi.yaml)
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT License - see [LICENSE](https://github.com/acenji/csv2geo-api/blob/main/LICENSE) for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "csv2geo-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js SDK for CSV2GEO Geocoding API",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/**/*"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
|
+
"lint": "eslint src/"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"geocoding",
|
|
16
|
+
"geocode",
|
|
17
|
+
"address",
|
|
18
|
+
"latitude",
|
|
19
|
+
"longitude",
|
|
20
|
+
"maps",
|
|
21
|
+
"csv2geo",
|
|
22
|
+
"geolocation"
|
|
23
|
+
],
|
|
24
|
+
"author": "Scale Campaign <admin@csv2geo.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/acenji/csv2geo-api.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/acenji/csv2geo-api/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://csv2geo.com",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"eslint": "^8.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom errors for CSV2GEO SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base error class for CSV2GEO SDK
|
|
7
|
+
*/
|
|
8
|
+
class CSV2GEOError extends Error {
|
|
9
|
+
constructor(message, code = null, status = null) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'CSV2GEOError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Raised when API key is missing, invalid, or revoked
|
|
19
|
+
*/
|
|
20
|
+
class AuthenticationError extends CSV2GEOError {
|
|
21
|
+
constructor(message, code = null, status = 401) {
|
|
22
|
+
super(message, code, status);
|
|
23
|
+
this.name = 'AuthenticationError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Raised when rate limit is exceeded
|
|
29
|
+
*/
|
|
30
|
+
class RateLimitError extends CSV2GEOError {
|
|
31
|
+
constructor(message, code = null, status = 429, retryAfter = null) {
|
|
32
|
+
super(message, code, status);
|
|
33
|
+
this.name = 'RateLimitError';
|
|
34
|
+
this.retryAfter = retryAfter;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Raised when request parameters are invalid
|
|
40
|
+
*/
|
|
41
|
+
class InvalidRequestError extends CSV2GEOError {
|
|
42
|
+
constructor(message, code = null, status = 400) {
|
|
43
|
+
super(message, code, status);
|
|
44
|
+
this.name = 'InvalidRequestError';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Raised when API key lacks required permission
|
|
50
|
+
*/
|
|
51
|
+
class PermissionError extends CSV2GEOError {
|
|
52
|
+
constructor(message, code = null, status = 403) {
|
|
53
|
+
super(message, code, status);
|
|
54
|
+
this.name = 'PermissionError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Raised for general API errors
|
|
60
|
+
*/
|
|
61
|
+
class APIError extends CSV2GEOError {
|
|
62
|
+
constructor(message, code = null, status = 500) {
|
|
63
|
+
super(message, code, status);
|
|
64
|
+
this.name = 'APIError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
CSV2GEOError,
|
|
70
|
+
AuthenticationError,
|
|
71
|
+
RateLimitError,
|
|
72
|
+
InvalidRequestError,
|
|
73
|
+
PermissionError,
|
|
74
|
+
APIError,
|
|
75
|
+
};
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV2GEO Node.js SDK Type Definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ClientOptions {
|
|
6
|
+
/** API base URL (default: https://api.csv2geo.com/v1) */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
/** Request timeout in milliseconds (default: 30000) */
|
|
9
|
+
timeout?: number;
|
|
10
|
+
/** Auto-retry on rate limit (default: true) */
|
|
11
|
+
autoRetry?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GeocodeOptions {
|
|
15
|
+
/** Limit to specific country (ISO 3166-1 alpha-2) */
|
|
16
|
+
country?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AddressComponents {
|
|
20
|
+
houseNumber?: string;
|
|
21
|
+
street?: string;
|
|
22
|
+
unit?: string;
|
|
23
|
+
city?: string;
|
|
24
|
+
state?: string;
|
|
25
|
+
postcode?: string;
|
|
26
|
+
country?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GeocodeResult {
|
|
30
|
+
formattedAddress: string;
|
|
31
|
+
lat: number;
|
|
32
|
+
lng: number;
|
|
33
|
+
accuracy: string;
|
|
34
|
+
accuracyScore: number;
|
|
35
|
+
components: AddressComponents;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GeocodeResponse {
|
|
39
|
+
query: string | { lat: number; lng: number };
|
|
40
|
+
results: GeocodeResult[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Coordinate {
|
|
44
|
+
lat: number;
|
|
45
|
+
lng: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class Client {
|
|
49
|
+
/** Max requests per minute */
|
|
50
|
+
rateLimit: string | null;
|
|
51
|
+
/** Requests remaining in current window */
|
|
52
|
+
rateLimitRemaining: string | null;
|
|
53
|
+
/** Unix timestamp when limit resets */
|
|
54
|
+
rateLimitReset: string | null;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a new CSV2GEO client
|
|
58
|
+
* @param apiKey Your CSV2GEO API key
|
|
59
|
+
* @param options Configuration options
|
|
60
|
+
*/
|
|
61
|
+
constructor(apiKey: string, options?: ClientOptions);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Geocode a single address
|
|
65
|
+
* @param address The address to geocode
|
|
66
|
+
* @param options Options
|
|
67
|
+
* @returns Best result or null if not found
|
|
68
|
+
*/
|
|
69
|
+
geocode(address: string, options?: GeocodeOptions): Promise<GeocodeResult | null>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Geocode with full response
|
|
73
|
+
* @param address The address to geocode
|
|
74
|
+
* @param options Options
|
|
75
|
+
* @returns Full response with all results
|
|
76
|
+
*/
|
|
77
|
+
geocodeFull(address: string, options?: GeocodeOptions): Promise<GeocodeResponse>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reverse geocode coordinates
|
|
81
|
+
* @param lat Latitude
|
|
82
|
+
* @param lng Longitude
|
|
83
|
+
* @returns Best result or null if not found
|
|
84
|
+
*/
|
|
85
|
+
reverse(lat: number, lng: number): Promise<GeocodeResult | null>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reverse geocode with full response
|
|
89
|
+
* @param lat Latitude
|
|
90
|
+
* @param lng Longitude
|
|
91
|
+
* @returns Full response with all results
|
|
92
|
+
*/
|
|
93
|
+
reverseFull(lat: number, lng: number): Promise<GeocodeResponse>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Batch geocode multiple addresses
|
|
97
|
+
* @param addresses Array of addresses (max 10,000)
|
|
98
|
+
* @returns Array of responses
|
|
99
|
+
*/
|
|
100
|
+
geocodeBatch(addresses: string[]): Promise<GeocodeResponse[]>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Batch reverse geocode multiple coordinates
|
|
104
|
+
* @param coordinates Array of coordinates (max 10,000)
|
|
105
|
+
* @returns Array of responses
|
|
106
|
+
*/
|
|
107
|
+
reverseBatch(coordinates: Coordinate[]): Promise<GeocodeResponse[]>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class CSV2GEOError extends Error {
|
|
111
|
+
code: string | null;
|
|
112
|
+
status: number | null;
|
|
113
|
+
constructor(message: string, code?: string | null, status?: number | null);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class AuthenticationError extends CSV2GEOError {}
|
|
117
|
+
|
|
118
|
+
export class RateLimitError extends CSV2GEOError {
|
|
119
|
+
retryAfter: number | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class InvalidRequestError extends CSV2GEOError {}
|
|
123
|
+
|
|
124
|
+
export class PermissionError extends CSV2GEOError {}
|
|
125
|
+
|
|
126
|
+
export class APIError extends CSV2GEOError {}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV2GEO Node.js SDK
|
|
3
|
+
*
|
|
4
|
+
* Fast, accurate geocoding powered by 446M+ addresses worldwide.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { Client } = require('csv2geo');
|
|
8
|
+
*
|
|
9
|
+
* const client = new Client('your_api_key');
|
|
10
|
+
* const result = await client.geocode('1600 Pennsylvania Ave, Washington DC');
|
|
11
|
+
* console.log(result.lat, result.lng);
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
CSV2GEOError,
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
InvalidRequestError,
|
|
19
|
+
PermissionError,
|
|
20
|
+
APIError,
|
|
21
|
+
} = require('./errors');
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE_URL = 'https://api.csv2geo.com/v1';
|
|
24
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
25
|
+
const MAX_RETRIES = 3;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CSV2GEO API Client
|
|
29
|
+
*/
|
|
30
|
+
class Client {
|
|
31
|
+
/**
|
|
32
|
+
* Create a new CSV2GEO client
|
|
33
|
+
* @param {string} apiKey - Your CSV2GEO API key
|
|
34
|
+
* @param {Object} [options] - Configuration options
|
|
35
|
+
* @param {string} [options.baseUrl] - API base URL
|
|
36
|
+
* @param {number} [options.timeout] - Request timeout in milliseconds
|
|
37
|
+
* @param {boolean} [options.autoRetry] - Auto-retry on rate limit (default: true)
|
|
38
|
+
*/
|
|
39
|
+
constructor(apiKey, options = {}) {
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
throw new Error('API key is required');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.apiKey = apiKey;
|
|
45
|
+
this.baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
46
|
+
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
47
|
+
this.autoRetry = options.autoRetry !== false;
|
|
48
|
+
|
|
49
|
+
// Rate limit tracking
|
|
50
|
+
this.rateLimit = null;
|
|
51
|
+
this.rateLimitRemaining = null;
|
|
52
|
+
this.rateLimitReset = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Make an API request
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
async _request(method, endpoint, params = {}, body = null, retryCount = 0) {
|
|
60
|
+
const url = new URL(`${this.baseUrl}${endpoint}`);
|
|
61
|
+
|
|
62
|
+
// Add query parameters
|
|
63
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
64
|
+
if (value !== undefined && value !== null) {
|
|
65
|
+
url.searchParams.append(key, value);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const fetchOptions = {
|
|
74
|
+
method,
|
|
75
|
+
headers: {
|
|
76
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
'User-Agent': 'csv2geo-node/1.0.0',
|
|
79
|
+
},
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (body) {
|
|
84
|
+
fetchOptions.body = JSON.stringify(body);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await fetch(url.toString(), fetchOptions);
|
|
88
|
+
clearTimeout(timeoutId);
|
|
89
|
+
|
|
90
|
+
// Update rate limit info
|
|
91
|
+
this.rateLimit = response.headers.get('X-RateLimit-Limit');
|
|
92
|
+
this.rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
|
|
93
|
+
this.rateLimitReset = response.headers.get('X-RateLimit-Reset');
|
|
94
|
+
|
|
95
|
+
const data = await response.json();
|
|
96
|
+
|
|
97
|
+
if (response.ok) {
|
|
98
|
+
return data;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Handle errors
|
|
102
|
+
const error = data.error || {};
|
|
103
|
+
const code = error.code || 'unknown';
|
|
104
|
+
const message = error.message || 'Unknown error';
|
|
105
|
+
const status = error.status || response.status;
|
|
106
|
+
|
|
107
|
+
if (response.status === 401) {
|
|
108
|
+
throw new AuthenticationError(message, code, status);
|
|
109
|
+
} else if (response.status === 403) {
|
|
110
|
+
throw new PermissionError(message, code, status);
|
|
111
|
+
} else if (response.status === 429) {
|
|
112
|
+
const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10);
|
|
113
|
+
const rateLimitError = new RateLimitError(message, code, status, retryAfter);
|
|
114
|
+
|
|
115
|
+
if (this.autoRetry && retryCount < MAX_RETRIES) {
|
|
116
|
+
await this._sleep(Math.min(retryAfter * 1000, 60000));
|
|
117
|
+
return this._request(method, endpoint, params, body, retryCount + 1);
|
|
118
|
+
}
|
|
119
|
+
throw rateLimitError;
|
|
120
|
+
} else if (response.status === 400) {
|
|
121
|
+
throw new InvalidRequestError(message, code, status);
|
|
122
|
+
} else {
|
|
123
|
+
throw new APIError(message, code, status);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
|
|
128
|
+
if (err.name === 'AbortError') {
|
|
129
|
+
throw new APIError('Request timed out', 'timeout');
|
|
130
|
+
}
|
|
131
|
+
if (err instanceof CSV2GEOError) {
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
throw new APIError(err.message, 'network_error');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Sleep helper
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_sleep(ms) {
|
|
143
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Geocode a single address
|
|
148
|
+
* @param {string} address - The address to geocode
|
|
149
|
+
* @param {Object} [options] - Options
|
|
150
|
+
* @param {string} [options.country] - Limit to specific country (ISO 3166-1 alpha-2)
|
|
151
|
+
* @returns {Promise<GeocodeResult|null>} Best result or null if not found
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* const result = await client.geocode('1600 Pennsylvania Ave, Washington DC');
|
|
155
|
+
* if (result) {
|
|
156
|
+
* console.log(result.lat, result.lng);
|
|
157
|
+
* }
|
|
158
|
+
*/
|
|
159
|
+
async geocode(address, options = {}) {
|
|
160
|
+
const params = { q: address };
|
|
161
|
+
if (options.country) params.country = options.country;
|
|
162
|
+
|
|
163
|
+
const data = await this._request('GET', '/geocode', params);
|
|
164
|
+
const results = data.results || [];
|
|
165
|
+
return results.length > 0 ? this._parseResult(results[0]) : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Geocode with full response
|
|
170
|
+
* @param {string} address - The address to geocode
|
|
171
|
+
* @param {Object} [options] - Options
|
|
172
|
+
* @returns {Promise<GeocodeResponse>} Full response with all results
|
|
173
|
+
*/
|
|
174
|
+
async geocodeFull(address, options = {}) {
|
|
175
|
+
const params = { q: address };
|
|
176
|
+
if (options.country) params.country = options.country;
|
|
177
|
+
|
|
178
|
+
const data = await this._request('GET', '/geocode', params);
|
|
179
|
+
return {
|
|
180
|
+
query: data.query,
|
|
181
|
+
results: (data.results || []).map(r => this._parseResult(r)),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Reverse geocode coordinates
|
|
187
|
+
* @param {number} lat - Latitude
|
|
188
|
+
* @param {number} lng - Longitude
|
|
189
|
+
* @returns {Promise<GeocodeResult|null>} Best result or null if not found
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* const result = await client.reverse(38.8977, -77.0365);
|
|
193
|
+
* if (result) {
|
|
194
|
+
* console.log(result.formattedAddress);
|
|
195
|
+
* }
|
|
196
|
+
*/
|
|
197
|
+
async reverse(lat, lng) {
|
|
198
|
+
const data = await this._request('GET', '/reverse', { lat, lng });
|
|
199
|
+
const results = data.results || [];
|
|
200
|
+
return results.length > 0 ? this._parseResult(results[0]) : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reverse geocode with full response
|
|
205
|
+
* @param {number} lat - Latitude
|
|
206
|
+
* @param {number} lng - Longitude
|
|
207
|
+
* @returns {Promise<GeocodeResponse>} Full response with all results
|
|
208
|
+
*/
|
|
209
|
+
async reverseFull(lat, lng) {
|
|
210
|
+
const data = await this._request('GET', '/reverse', { lat, lng });
|
|
211
|
+
return {
|
|
212
|
+
query: data.query,
|
|
213
|
+
results: (data.results || []).map(r => this._parseResult(r)),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Batch geocode multiple addresses
|
|
219
|
+
* @param {string[]} addresses - Array of addresses (max 10,000)
|
|
220
|
+
* @returns {Promise<GeocodeResponse[]>} Array of responses
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* const results = await client.geocodeBatch([
|
|
224
|
+
* '1600 Pennsylvania Ave, Washington DC',
|
|
225
|
+
* '350 Fifth Avenue, New York, NY',
|
|
226
|
+
* ]);
|
|
227
|
+
*/
|
|
228
|
+
async geocodeBatch(addresses) {
|
|
229
|
+
if (addresses.length > 10000) {
|
|
230
|
+
throw new InvalidRequestError('Maximum 10,000 addresses per batch request');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const data = await this._request('POST', '/geocode', {}, { addresses });
|
|
234
|
+
return (data.results || []).map(r => ({
|
|
235
|
+
query: r.query,
|
|
236
|
+
results: (r.results || []).map(res => this._parseResult(res)),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Batch reverse geocode multiple coordinates
|
|
242
|
+
* @param {Array<{lat: number, lng: number}>} coordinates - Array of coordinates (max 10,000)
|
|
243
|
+
* @returns {Promise<GeocodeResponse[]>} Array of responses
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* const results = await client.reverseBatch([
|
|
247
|
+
* { lat: 38.8977, lng: -77.0365 },
|
|
248
|
+
* { lat: 40.7484, lng: -73.9857 },
|
|
249
|
+
* ]);
|
|
250
|
+
*/
|
|
251
|
+
async reverseBatch(coordinates) {
|
|
252
|
+
if (coordinates.length > 10000) {
|
|
253
|
+
throw new InvalidRequestError('Maximum 10,000 coordinates per batch request');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const data = await this._request('POST', '/reverse', {}, { coordinates });
|
|
257
|
+
return (data.results || []).map(r => ({
|
|
258
|
+
query: r.query,
|
|
259
|
+
results: (r.results || []).map(res => this._parseResult(res)),
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Parse API result into GeocodeResult
|
|
265
|
+
* @private
|
|
266
|
+
*/
|
|
267
|
+
_parseResult(data) {
|
|
268
|
+
const location = data.location || {};
|
|
269
|
+
return {
|
|
270
|
+
formattedAddress: data.formatted_address,
|
|
271
|
+
lat: location.lat,
|
|
272
|
+
lng: location.lng,
|
|
273
|
+
accuracy: data.accuracy,
|
|
274
|
+
accuracyScore: data.accuracy_score,
|
|
275
|
+
components: {
|
|
276
|
+
houseNumber: data.components?.house_number,
|
|
277
|
+
street: data.components?.street,
|
|
278
|
+
unit: data.components?.unit,
|
|
279
|
+
city: data.components?.city,
|
|
280
|
+
state: data.components?.state,
|
|
281
|
+
postcode: data.components?.postcode,
|
|
282
|
+
country: data.components?.country,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
Client,
|
|
290
|
+
CSV2GEOError,
|
|
291
|
+
AuthenticationError,
|
|
292
|
+
RateLimitError,
|
|
293
|
+
InvalidRequestError,
|
|
294
|
+
PermissionError,
|
|
295
|
+
APIError,
|
|
296
|
+
};
|