geocodio-library-node 1.11.0 → 1.15.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/lib/index.js CHANGED
@@ -4,18 +4,138 @@ const axios = require("axios");
4
4
  const fs = require("fs");
5
5
  const FormData = require("form-data");
6
6
 
7
+ // Distance API Enums
8
+ const DistanceMode = {
9
+ Straightline: "straightline",
10
+ Driving: "driving",
11
+ Haversine: "haversine" // Alias for straightline (backward compat)
12
+ };
13
+
14
+ const DistanceUnits = {
15
+ Miles: "miles",
16
+ Kilometers: "km",
17
+ Km: "km" // Alias
18
+ };
19
+
20
+ const DistanceOrderBy = {
21
+ Distance: "distance",
22
+ Duration: "duration"
23
+ };
24
+
25
+ const DistanceSortOrder = {
26
+ Asc: "asc",
27
+ Desc: "desc"
28
+ };
29
+
30
+ // Coordinate class for distance calculations
31
+ class Coordinate {
32
+ constructor(lat, lng, id) {
33
+ if (typeof lat !== "number" || isNaN(lat)) {
34
+ throw new Error(`Latitude must be a number, got ${typeof lat}`);
35
+ }
36
+
37
+ if (typeof lng !== "number" || isNaN(lng)) {
38
+ throw new Error(`Longitude must be a number, got ${typeof lng}`);
39
+ }
40
+
41
+ if (lat < -90 || lat > 90) {
42
+ throw new Error(`Latitude must be between -90 and 90, got ${lat}`);
43
+ }
44
+
45
+ if (lng < -180 || lng > 180) {
46
+ throw new Error(`Longitude must be between -180 and 180, got ${lng}`);
47
+ }
48
+
49
+ this.lat = lat;
50
+ this.lng = lng;
51
+ this.id = id || undefined;
52
+ }
53
+
54
+ static from(input) {
55
+ if (input instanceof Coordinate) {
56
+ return input;
57
+ }
58
+
59
+ if (typeof input === "string") {
60
+ return Coordinate.fromString(input);
61
+ }
62
+
63
+ if (Array.isArray(input)) {
64
+ return Coordinate.fromArray(input);
65
+ }
66
+
67
+ if (typeof input === "object" && input !== null) {
68
+ return new Coordinate(input.lat, input.lng, input.id);
69
+ }
70
+
71
+ throw new Error(`Invalid coordinate input: ${input}`);
72
+ }
73
+
74
+ static fromString(input) {
75
+ const parts = input.split(",").map(p => p.trim());
76
+ if (parts.length < 2) {
77
+ throw new Error(`Invalid coordinate string: ${input}`);
78
+ }
79
+
80
+ const lat = parseFloat(parts[0]);
81
+ const lng = parseFloat(parts[1]);
82
+ const id = parts[2] || undefined;
83
+
84
+ if (isNaN(lat) || isNaN(lng)) {
85
+ throw new Error(`Invalid coordinate values in string: ${input}`);
86
+ }
87
+
88
+ return new Coordinate(lat, lng, id);
89
+ }
90
+
91
+ static fromArray(input) {
92
+ if (input.length < 2) {
93
+ throw new Error(`Coordinate array must have at least 2 elements`);
94
+ }
95
+
96
+ const lat = parseFloat(input[0]);
97
+ const lng = parseFloat(input[1]);
98
+ const id = input[2] === undefined ? undefined : String(input[2]);
99
+
100
+ return new Coordinate(lat, lng, id);
101
+ }
102
+
103
+ // For GET requests (string format): "lat,lng" or "lat,lng,id"
104
+ toString() {
105
+ let str = `${this.lat},${this.lng}`;
106
+ if (this.id !== undefined) {
107
+ str += `,${this.id}`;
108
+ }
109
+
110
+ return str;
111
+ }
112
+
113
+ // For POST requests (object format): {lat, lng, id?}
114
+ toObject() {
115
+ const obj = {
116
+ lat: this.lat,
117
+ lng: this.lng
118
+ };
119
+ if (this.id !== undefined) {
120
+ obj.id = this.id;
121
+ }
122
+
123
+ return obj;
124
+ }
125
+ }
126
+
7
127
  class Geocodio {
8
128
  constructor(apiKey, hostname, apiVersion) {
9
129
  this.apiKey = apiKey || process.env.GEOCODIO_API_KEY || null;
10
130
  this.hostname =
11
131
  hostname || process.env.GEOCODIO_HOSTNAME || "api.geocod.io";
12
- this.apiVersion = apiVersion || process.env.GEOCODIO_API_VERSION || "v1.8";
132
+ this.apiVersion = apiVersion || process.env.GEOCODIO_API_VERSION || "v1.11";
13
133
 
14
134
  this.SINGLE_TIMEOUT_MS = 5000;
15
135
  this.BATCH_TIMEOUT_MS = 30 * 60 * 1000;
16
136
 
17
137
  this.HTTP_HEADERS = {
18
- "User-Agent": "geocodio-library-node/1.10.0",
138
+ "User-Agent": "geocodio-library-node/1.15.0",
19
139
  Authorization: `Bearer ${this.apiKey}`
20
140
  };
21
141
 
@@ -30,15 +150,343 @@ class Geocodio {
30
150
  this.list = this.list();
31
151
  }
32
152
 
33
- geocode(query, fields = [], limit = null) {
34
- return this.handleRequest("geocode", query, fields, limit);
153
+ // Helper: Serialize params with array support (e.g., destinations[])
154
+ serializeParams(params) {
155
+ const parts = [];
156
+
157
+ for (const key in params) {
158
+ if (Object.prototype.hasOwnProperty.call(params, key)) {
159
+ const value = params[key];
160
+
161
+ if (Array.isArray(value)) {
162
+ value.forEach(v => {
163
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
164
+ });
165
+ } else if (value !== undefined && value !== null && value !== "") {
166
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
167
+ }
168
+ }
169
+ }
170
+
171
+ return parts.join("&");
172
+ }
173
+
174
+ geocode(query, fields = [], limit = null, distanceOptions = null) {
175
+ return this.handleRequest("geocode", query, fields, limit, distanceOptions);
176
+ }
177
+
178
+ reverse(query, fields = [], limit = null, distanceOptions = null) {
179
+ return this.handleRequest("reverse", query, fields, limit, distanceOptions);
35
180
  }
36
181
 
37
- reverse(query, fields = [], limit = null) {
38
- return this.handleRequest("reverse", query, fields, limit);
182
+ // Distance API - Single origin to multiple destinations (GET)
183
+ distance(origin, destinations, options = {}) {
184
+ const url = this.formatUrl("distance");
185
+ const originCoord = Coordinate.from(origin);
186
+
187
+ const queryParameters = {
188
+ origin: originCoord.toString()
189
+ };
190
+
191
+ // Add destinations as array params
192
+ const destStrings = destinations.map(d => Coordinate.from(d).toString());
193
+ queryParameters["destinations[]"] = destStrings;
194
+
195
+ // Add distance options
196
+ this.addDistanceOptionsToParams(queryParameters, options);
197
+
198
+ return this.handleResponse(
199
+ axios.get(url, {
200
+ params: queryParameters,
201
+ timeout: this.SINGLE_TIMEOUT_MS,
202
+ headers: this.HTTP_HEADERS,
203
+ paramsSerializer: params => this.serializeParams(params)
204
+ })
205
+ );
206
+ }
207
+
208
+ // Distance Matrix API - Multiple origins × destinations (POST)
209
+ distanceMatrix(origins, destinations, options = {}) {
210
+ const url = this.formatUrl("distance-matrix");
211
+
212
+ // Convert to object format for POST requests
213
+ const originsObjects = origins.map(o => Coordinate.from(o).toObject());
214
+ const destObjects = destinations.map(d => Coordinate.from(d).toObject());
215
+
216
+ const body = {
217
+ origins: originsObjects,
218
+ destinations: destObjects
219
+ };
220
+
221
+ // Add distance options to body
222
+ this.addDistanceOptionsToBody(body, options);
223
+
224
+ return this.handleResponse(
225
+ axios.post(url, body, {
226
+ timeout: this.BATCH_TIMEOUT_MS,
227
+ headers: this.HTTP_HEADERS
228
+ })
229
+ );
39
230
  }
40
231
 
41
- handleRequest(endpoint, query, fields = [], limit = null) {
232
+ // Async Distance Matrix Job - Create
233
+ createDistanceMatrixJob(name, origins, destinations, options = {}) {
234
+ const url = this.formatUrl("distance-jobs");
235
+
236
+ const body = {
237
+ name
238
+ };
239
+
240
+ // Origins can be array of coordinates or list ID (number)
241
+ if (typeof origins === "number") {
242
+ body.origins = origins;
243
+ } else {
244
+ body.origins = origins.map(o => Coordinate.from(o).toObject());
245
+ }
246
+
247
+ // Destinations can be array of coordinates or list ID (number)
248
+ if (typeof destinations === "number") {
249
+ body.destinations = destinations;
250
+ } else {
251
+ body.destinations = destinations.map(d => Coordinate.from(d).toObject());
252
+ }
253
+
254
+ // Add distance options to body
255
+ this.addDistanceOptionsToBody(body, options);
256
+
257
+ // Add callback URL if provided
258
+ if (options.callbackUrl) {
259
+ body.callback_url = options.callbackUrl;
260
+ }
261
+
262
+ return this.handleResponse(
263
+ axios.post(url, body, {
264
+ timeout: this.SINGLE_TIMEOUT_MS,
265
+ headers: this.HTTP_HEADERS
266
+ })
267
+ );
268
+ }
269
+
270
+ // Async Distance Matrix Job - Get Status
271
+ distanceMatrixJobStatus(id) {
272
+ const url = this.formatUrl(`distance-jobs/${id}`);
273
+
274
+ return this.handleResponse(
275
+ axios.get(url, {
276
+ timeout: this.SINGLE_TIMEOUT_MS,
277
+ headers: this.HTTP_HEADERS
278
+ })
279
+ );
280
+ }
281
+
282
+ // Async Distance Matrix Job - List All
283
+ distanceMatrixJobs(page = null) {
284
+ const url = this.formatUrl("distance-jobs");
285
+
286
+ const params = {};
287
+ if (page !== null) {
288
+ params.page = page;
289
+ }
290
+
291
+ return this.handleResponse(
292
+ axios.get(url, {
293
+ params,
294
+ timeout: this.SINGLE_TIMEOUT_MS,
295
+ headers: this.HTTP_HEADERS
296
+ })
297
+ );
298
+ }
299
+
300
+ // Async Distance Matrix Job - Get Results
301
+ getDistanceMatrixJobResults(id) {
302
+ const url = this.formatUrl(`distance-jobs/${id}/download`);
303
+
304
+ return this.handleResponse(
305
+ axios.get(url, {
306
+ timeout: this.BATCH_TIMEOUT_MS,
307
+ headers: this.HTTP_HEADERS
308
+ })
309
+ );
310
+ }
311
+
312
+ // Async Distance Matrix Job - Download to File
313
+ downloadDistanceMatrixJob(id, filePath) {
314
+ const url = this.formatUrl(`distance-jobs/${id}/download`);
315
+ const writer = fs.createWriteStream(filePath);
316
+
317
+ return this.handleResponse(
318
+ axios({
319
+ method: "get",
320
+ url,
321
+ headers: this.HTTP_HEADERS,
322
+ responseType: "stream"
323
+ }).then(response => {
324
+ return new Promise((resolve, reject) => {
325
+ response.data.pipe(writer);
326
+ let error = null;
327
+ writer.on("error", err => {
328
+ error = err;
329
+ writer.close();
330
+ reject(err);
331
+ });
332
+ writer.on("close", () => {
333
+ if (!error) {
334
+ resolve(true);
335
+ }
336
+ });
337
+ });
338
+ })
339
+ );
340
+ }
341
+
342
+ // Async Distance Matrix Job - Delete
343
+ deleteDistanceMatrixJob(id) {
344
+ const url = this.formatUrl(`distance-jobs/${id}`);
345
+
346
+ return this.handleResponse(
347
+ axios.delete(url, {
348
+ timeout: this.SINGLE_TIMEOUT_MS,
349
+ headers: this.HTTP_HEADERS
350
+ })
351
+ );
352
+ }
353
+
354
+ // Helper: Add distance options to GET query parameters
355
+ addDistanceOptionsToParams(params, options) {
356
+ if (options.mode) {
357
+ params.mode = options.mode;
358
+ }
359
+
360
+ if (options.units) {
361
+ params.units = options.units;
362
+ }
363
+
364
+ if (options.maxResults !== undefined) {
365
+ params.max_results = options.maxResults;
366
+ }
367
+
368
+ if (options.maxDistance !== undefined) {
369
+ params.max_distance = options.maxDistance;
370
+ }
371
+
372
+ if (options.maxDuration !== undefined) {
373
+ params.max_duration = options.maxDuration;
374
+ }
375
+
376
+ if (options.minDistance !== undefined) {
377
+ params.min_distance = options.minDistance;
378
+ }
379
+
380
+ if (options.minDuration !== undefined) {
381
+ params.min_duration = options.minDuration;
382
+ }
383
+
384
+ if (options.orderBy) {
385
+ params.order_by = options.orderBy;
386
+ }
387
+
388
+ if (options.sortOrder) {
389
+ params.sort = options.sortOrder;
390
+ }
391
+ }
392
+
393
+ // Helper: Add distance options to POST body
394
+ addDistanceOptionsToBody(body, options) {
395
+ if (options.mode) {
396
+ body.mode = options.mode;
397
+ }
398
+
399
+ if (options.units) {
400
+ body.units = options.units;
401
+ }
402
+
403
+ if (options.maxResults !== undefined) {
404
+ body.max_results = options.maxResults;
405
+ }
406
+
407
+ if (options.maxDistance !== undefined) {
408
+ body.max_distance = options.maxDistance;
409
+ }
410
+
411
+ if (options.maxDuration !== undefined) {
412
+ body.max_duration = options.maxDuration;
413
+ }
414
+
415
+ if (options.minDistance !== undefined) {
416
+ body.min_distance = options.minDistance;
417
+ }
418
+
419
+ if (options.minDuration !== undefined) {
420
+ body.min_duration = options.minDuration;
421
+ }
422
+
423
+ if (options.orderBy) {
424
+ body.order_by = options.orderBy;
425
+ }
426
+
427
+ if (options.sortOrder) {
428
+ body.sort = options.sortOrder;
429
+ }
430
+ }
431
+
432
+ // Helper: Add distance options for geocode/reverse
433
+ addDistanceOptionsToGeocodeParams(params, distanceOptions) {
434
+ if (!distanceOptions) return;
435
+
436
+ if (
437
+ distanceOptions.destinations &&
438
+ distanceOptions.destinations.length > 0
439
+ ) {
440
+ const destStrings = distanceOptions.destinations.map(d =>
441
+ Coordinate.from(d).toString()
442
+ );
443
+ params["destinations[]"] = destStrings;
444
+ }
445
+
446
+ if (distanceOptions.distanceMode) {
447
+ params.distance_mode = distanceOptions.distanceMode;
448
+ }
449
+
450
+ if (distanceOptions.distanceUnits) {
451
+ params.distance_units = distanceOptions.distanceUnits;
452
+ }
453
+
454
+ if (distanceOptions.distanceMaxResults !== undefined) {
455
+ params.distance_max_results = distanceOptions.distanceMaxResults;
456
+ }
457
+
458
+ if (distanceOptions.distanceMaxDistance !== undefined) {
459
+ params.distance_max_distance = distanceOptions.distanceMaxDistance;
460
+ }
461
+
462
+ if (distanceOptions.distanceMaxDuration !== undefined) {
463
+ params.distance_max_duration = distanceOptions.distanceMaxDuration;
464
+ }
465
+
466
+ if (distanceOptions.distanceMinDistance !== undefined) {
467
+ params.distance_min_distance = distanceOptions.distanceMinDistance;
468
+ }
469
+
470
+ if (distanceOptions.distanceMinDuration !== undefined) {
471
+ params.distance_min_duration = distanceOptions.distanceMinDuration;
472
+ }
473
+
474
+ if (distanceOptions.distanceOrderBy) {
475
+ params.distance_order_by = distanceOptions.distanceOrderBy;
476
+ }
477
+
478
+ if (distanceOptions.distanceSortOrder) {
479
+ params.distance_sort = distanceOptions.distanceSortOrder;
480
+ }
481
+ }
482
+
483
+ handleRequest(
484
+ endpoint,
485
+ query,
486
+ fields = [],
487
+ limit = null,
488
+ distanceOptions = null
489
+ ) {
42
490
  const url = this.formatUrl(endpoint);
43
491
 
44
492
  let queryParameters = {
@@ -49,11 +497,24 @@ class Geocodio {
49
497
  queryParameters.limit = limit;
50
498
  }
51
499
 
500
+ // Add distance options for geocode/reverse
501
+ this.addDistanceOptionsToGeocodeParams(queryParameters, distanceOptions);
502
+
52
503
  query = this.preprocessQuery(query, endpoint);
53
504
 
54
505
  let response = null;
506
+ const hasDistanceOptions =
507
+ distanceOptions &&
508
+ distanceOptions.destinations &&
509
+ distanceOptions.destinations.length > 0;
510
+
55
511
  if (this.isSingleQuery(query)) {
56
- response = this.performSingleRequest(url, query, queryParameters);
512
+ response = this.performSingleRequest(
513
+ url,
514
+ query,
515
+ queryParameters,
516
+ hasDistanceOptions
517
+ );
57
518
  } else {
58
519
  query = this.preprocessQueryList(query, endpoint);
59
520
  response = this.performBatchRequest(url, query, queryParameters);
@@ -125,7 +586,12 @@ class Geocodio {
125
586
  return true;
126
587
  }
127
588
 
128
- performSingleRequest(url, query, queryParameters) {
589
+ performSingleRequest(
590
+ url,
591
+ query,
592
+ queryParameters,
593
+ useArrayParamsSerializer = false
594
+ ) {
129
595
  if (typeof query === "object") {
130
596
  queryParameters = {
131
597
  ...queryParameters,
@@ -135,11 +601,18 @@ class Geocodio {
135
601
  queryParameters.q = query;
136
602
  }
137
603
 
138
- return axios.get(url, {
604
+ const axiosConfig = {
139
605
  params: queryParameters,
140
606
  timeout: this.SINGLE_TIMEOUT_MS,
141
607
  headers: this.HTTP_HEADERS
142
- });
608
+ };
609
+
610
+ // Use custom params serializer when we have array parameters (e.g., destinations[])
611
+ if (useArrayParamsSerializer) {
612
+ axiosConfig.paramsSerializer = params => this.serializeParams(params);
613
+ }
614
+
615
+ return axios.get(url, axiosConfig);
143
616
  }
144
617
 
145
618
  performBatchRequest(url, queries, queryParameters) {
@@ -239,4 +712,13 @@ class Geocodio {
239
712
  }
240
713
  }
241
714
 
715
+ // Default export for backward compatibility
242
716
  module.exports = Geocodio;
717
+
718
+ // Named exports for ES modules and additional utilities
719
+ module.exports.Geocodio = Geocodio;
720
+ module.exports.Coordinate = Coordinate;
721
+ module.exports.DistanceMode = DistanceMode;
722
+ module.exports.DistanceUnits = DistanceUnits;
723
+ module.exports.DistanceOrderBy = DistanceOrderBy;
724
+ module.exports.DistanceSortOrder = DistanceSortOrder;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "geocodio-library-node",
3
- "version": "1.11.0",
3
+ "version": "1.15.0",
4
4
  "description": "geocod.io geocoding API library",
5
5
  "homepage": "https://github.com/Geocodio/geocodio-library-node",
6
6
  "author": {
@@ -18,6 +18,8 @@
18
18
  "geocode",
19
19
  "geo",
20
20
  "address",
21
+ "distance",
22
+ "distance-matrix",
21
23
  "congress",
22
24
  "timezone",
23
25
  "census"