js-zip-code-locator 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/PUBLISH.md +27 -0
- package/README.md +200 -0
- package/index.js +349 -0
- package/package.json +11 -0
- package/test/index.js +308 -0
- package/zip-codes.json +1 -0
package/PUBLISH.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Publishing js-zip-code-locator to npm
|
|
2
|
+
|
|
3
|
+
Same flow as [git-config-parser](https://www.npmjs.com/package/git-config-parser): unscoped package, standard publish.
|
|
4
|
+
|
|
5
|
+
## Commands (from package root)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Go to the package folder
|
|
9
|
+
cd "/Users/fred/DEVELOPMENT/OPEN SOURCE/js-zip-code-locator"
|
|
10
|
+
|
|
11
|
+
# 2. Log in to npm (if you aren’t already)
|
|
12
|
+
npm login
|
|
13
|
+
|
|
14
|
+
# 3. Run tests
|
|
15
|
+
npm test
|
|
16
|
+
|
|
17
|
+
# 4. Publish
|
|
18
|
+
npm publish
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That’s it. The package will be at **https://www.npmjs.com/package/js-zip-code-locator**.
|
|
22
|
+
|
|
23
|
+
## Updating after changes
|
|
24
|
+
|
|
25
|
+
1. Bump version in `package.json` (e.g. `1.0.0` → `1.0.1` or `1.1.0`).
|
|
26
|
+
2. Run `npm test`.
|
|
27
|
+
3. Run `npm publish` again.
|
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# js-zip-code-locator
|
|
2
|
+
|
|
3
|
+
Node.js module for US ZIP code lookups: full data, coordinates, lookup by city/state, closest zips by radius, data by coordinates, distance between zips, state/city discovery, validation, bounding box, and more. Distances can be in **miles** (default) or **kilometers**.
|
|
4
|
+
|
|
5
|
+
Installable as a dependency in other projects.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install js-zip-code-locator
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
The module initializes with an optional unit: **miles** (default) or **kilometers**.
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const createZipLocator = require('js-zip-code-locator');
|
|
19
|
+
|
|
20
|
+
// Default: distances in miles
|
|
21
|
+
const locator = createZipLocator();
|
|
22
|
+
|
|
23
|
+
// Distances in kilometers
|
|
24
|
+
const locatorKm = createZipLocator({ useMiles: false });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
You can also use the default instance without calling the factory:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const zipLocator = require('js-zip-code-locator');
|
|
31
|
+
zipLocator.getZipData('10001');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## API Reference
|
|
37
|
+
|
|
38
|
+
### Lookup by ZIP
|
|
39
|
+
|
|
40
|
+
| Method | Description |
|
|
41
|
+
|--------|-------------|
|
|
42
|
+
| **`getZipData(zipCode)`** | Full record: state, city, lat, lng, and full state name. One object or `undefined`. |
|
|
43
|
+
| **`getZipCoordinates(zipCode)`** | `{ latitude, longitude }` or `undefined`. |
|
|
44
|
+
| **`getCityState(zipCode)`** | `{ city, state, formatted: "City, ST" }` or `undefined`. For labels. |
|
|
45
|
+
| **`isValidZip(zipCode)`** | `true` if the zip exists in the dataset, `false` otherwise. |
|
|
46
|
+
|
|
47
|
+
### Lookup by city / state
|
|
48
|
+
|
|
49
|
+
| Method | Description |
|
|
50
|
+
|--------|-------------|
|
|
51
|
+
| **`getZipCodes(city, state)`** | All zip codes (with coordinates) for a city and state. Array; can be multiple. |
|
|
52
|
+
| **`getZipsInState(state)`** | All zip codes (with coordinates) in a state. |
|
|
53
|
+
| **`getCitiesInState(state)`** | Unique city names in a state, sorted. |
|
|
54
|
+
| **`getFullStateName(stateAbbr)`** | Full state name for abbreviation (e.g. `'AL'` → `'Alabama'`). |
|
|
55
|
+
|
|
56
|
+
### Search and filters
|
|
57
|
+
|
|
58
|
+
| Method | Description |
|
|
59
|
+
|--------|-------------|
|
|
60
|
+
| **`searchZipPrefix(prefix)`** | Zips whose code starts with `prefix` (e.g. `'100'` for NYC area). |
|
|
61
|
+
| **`getZipsInBoundingBox(minLat, maxLat, minLng, maxLng)`** | All zips inside the given rectangle (e.g. map viewport). |
|
|
62
|
+
|
|
63
|
+
### Distance and radius
|
|
64
|
+
|
|
65
|
+
| Method | Description |
|
|
66
|
+
|--------|-------------|
|
|
67
|
+
| **`getClosestByZipCode(zipCode, radius, useMiles?)`** | Zips within `radius` of a zip, with distance. Unit: instance default or override. |
|
|
68
|
+
| **`getDataByCoordinates(lat, lng, options?)`** | All zips with distance from `(lat, lng)`. Options: `{ sort: 'distance' \| 'zip' \| 'city' }`. |
|
|
69
|
+
| **`getDistanceBetweenZipCodes(zip1, zip2, useMiles?)`** | Distance between two zips. `undefined` if either zip not found. |
|
|
70
|
+
| **`getDistancesFromZip(originZip, zipCodes, useMiles?)`** | Distances from one origin zip to many zips in one call. |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
### By ZIP code
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const locator = createZipLocator();
|
|
80
|
+
|
|
81
|
+
locator.getZipData('35004');
|
|
82
|
+
// { zip: '35004', latitude: 33.606379, longitude: -86.50249, city: 'Moody', state: 'AL', full: 'Alabama' }
|
|
83
|
+
|
|
84
|
+
locator.getZipCoordinates('35004');
|
|
85
|
+
// { latitude: 33.606379, longitude: -86.50249 }
|
|
86
|
+
|
|
87
|
+
locator.getCityState('35004');
|
|
88
|
+
// { city: 'Moody', state: 'AL', formatted: 'Moody, AL' }
|
|
89
|
+
|
|
90
|
+
locator.isValidZip('35004'); // true
|
|
91
|
+
locator.isValidZip('99999'); // false
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### By city and state
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
locator.getZipCodes('Moody', 'AL');
|
|
98
|
+
// [ { zip: '35004', latitude: ..., longitude: ..., city: 'Moody', state: 'AL', full: 'Alabama' }, ... ]
|
|
99
|
+
|
|
100
|
+
locator.getZipsInState('AL');
|
|
101
|
+
// [ { zip: '35004', ... }, { zip: '35005', ... }, ... ] (all zips in Alabama)
|
|
102
|
+
|
|
103
|
+
locator.getCitiesInState('AL');
|
|
104
|
+
// [ 'Adamsville', 'Adger', 'Alabaster', 'Alexander City', ... ]
|
|
105
|
+
|
|
106
|
+
locator.getFullStateName('AL'); // 'Alabama'
|
|
107
|
+
locator.getFullStateName('al'); // 'Alabama'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Search and bounding box
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
locator.searchZipPrefix('100');
|
|
114
|
+
// [ { zip: '10001', ... }, { zip: '10002', ... }, ... ] (NYC area)
|
|
115
|
+
|
|
116
|
+
locator.getZipsInBoundingBox(33.5, 34, -87, -86);
|
|
117
|
+
// [ { zip: '35004', ... }, ... ] (zips inside the lat/lng rectangle)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Distance and radius
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
locator.getClosestByZipCode('35004', 10);
|
|
124
|
+
// [ { zip: '35004', ..., distance: 0 }, { zip: '35005', ..., distance: 5.2 }, ... ]
|
|
125
|
+
|
|
126
|
+
locator.getClosestByZipCode('35004', 16, false); // radius and distance in km
|
|
127
|
+
|
|
128
|
+
locator.getDataByCoordinates(33.6, -86.5, { sort: 'distance' });
|
|
129
|
+
// [ { zip: '...', ..., distance: 2.1 }, ... ]
|
|
130
|
+
|
|
131
|
+
locator.getDataByCoordinates(33.6, -86.5, { sort: 'zip' }); // sort by zip
|
|
132
|
+
locator.getDataByCoordinates(33.6, -86.5, { sort: 'city' }); // sort by city
|
|
133
|
+
|
|
134
|
+
locator.getDistanceBetweenZipCodes('35004', '35007');
|
|
135
|
+
// 12.34 (miles)
|
|
136
|
+
|
|
137
|
+
locator.getDistanceBetweenZipCodes('35004', '35007', false);
|
|
138
|
+
// 19.86 (km)
|
|
139
|
+
|
|
140
|
+
locator.getDistancesFromZip('35004', ['35007', '35005', '99999']);
|
|
141
|
+
// [ { zip: '35007', distance: 12.34 }, { zip: '35005', distance: 5.2 }, { zip: '99999', distance: undefined } ]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Custom data file
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
const locator = createZipLocator({ dataPath: '/path/to/zip-codes.json' });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Expected JSON format: array of objects with `zip`, `latitude`, `longitude`, `city`, `state`, and optional `full`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Testing this project locally
|
|
155
|
+
|
|
156
|
+
1. **Clone and install** (if not already):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cd js-zip-code-locator
|
|
160
|
+
npm install
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
2. **Run the test suite**:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm test
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This runs Node’s built-in test runner (`node --test`) on the files in `test/`. No extra test framework is required.
|
|
170
|
+
|
|
171
|
+
3. **Run a single test file** (optional):
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
node --test test/index.js
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
4. **Try the API in Node** (optional):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
node -e "
|
|
181
|
+
const createZipLocator = require('.');
|
|
182
|
+
const locator = createZipLocator();
|
|
183
|
+
console.log(locator.getZipData('35004'));
|
|
184
|
+
console.log(locator.getCityState('35004'));
|
|
185
|
+
console.log(locator.getFullStateName('AL'));
|
|
186
|
+
console.log(locator.getDistanceBetweenZipCodes('35004', '35007'));
|
|
187
|
+
"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Contributing
|
|
191
|
+
|
|
192
|
+
Contributions are welcome. Anyone can contribute.
|
|
193
|
+
|
|
194
|
+
- Open an issue to discuss changes or report bugs
|
|
195
|
+
- Submit pull requests for fixes or new features
|
|
196
|
+
- Ensure tests pass with `npm test` before submitting
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const EARTH_RADIUS_MILES = 3958;
|
|
7
|
+
const EARTH_RADIUS_KM = 6371;
|
|
8
|
+
const DEFAULT_DATA_PATH = path.join(__dirname, 'zip-codes.json');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize zip to 5-character string (zero-padded).
|
|
12
|
+
* @param {string|number} zip
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function normalizeZip(zip) {
|
|
16
|
+
const s = String(zip).trim();
|
|
17
|
+
return s.length <= 5 ? s.padStart(5, '0') : s.slice(0, 5);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Haversine distance between two lat/lng points.
|
|
22
|
+
* @param {number} lat1
|
|
23
|
+
* @param {number} lng1
|
|
24
|
+
* @param {number} lat2
|
|
25
|
+
* @param {number} lng2
|
|
26
|
+
* @param {boolean} [useMiles=true]
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
function haversineDistance(lat1, lng1, lat2, lng2, useMiles = true) {
|
|
30
|
+
const toRad = (x) => (x * Math.PI) / 180;
|
|
31
|
+
const R = useMiles ? EARTH_RADIUS_MILES : EARTH_RADIUS_KM;
|
|
32
|
+
const dLat = toRad(lat2 - lat1);
|
|
33
|
+
const dLng = toRad(lng2 - lng1);
|
|
34
|
+
const a =
|
|
35
|
+
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
36
|
+
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
|
|
37
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
38
|
+
return R * c;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @deprecated Use haversineDistance(..., useMiles) */
|
|
42
|
+
function distanceMiles(lat1, lng1, lat2, lng2) {
|
|
43
|
+
return haversineDistance(lat1, lng1, lat2, lng2, true);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize city/state for index key (lowercase, trimmed).
|
|
48
|
+
* @param {string} city
|
|
49
|
+
* @param {string} state
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
function cityStateKey(city, state) {
|
|
53
|
+
return `${String(city).toLowerCase().trim()}|${String(state).toLowerCase().trim()}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize state to 2-char uppercase for indexing.
|
|
58
|
+
* @param {string} state
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function normalizeState(state) {
|
|
62
|
+
return String(state).toUpperCase().trim().slice(0, 2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load and index zip data from a JSON file.
|
|
67
|
+
* Expected format: array of { zip, latitude, longitude, city, state, full }.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} [dataPath] - Path to zip-codes.json.
|
|
70
|
+
* @returns {object} Internal indexed data (byZip, byCityState, list).
|
|
71
|
+
*/
|
|
72
|
+
function loadZipData(dataPath = DEFAULT_DATA_PATH) {
|
|
73
|
+
const raw = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
|
|
74
|
+
if (!Array.isArray(raw)) {
|
|
75
|
+
throw new Error('Zip data must be an array of objects');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const list = raw.map((r) => ({
|
|
79
|
+
zip: normalizeZip(r.zip),
|
|
80
|
+
latitude: Number(r.latitude),
|
|
81
|
+
longitude: Number(r.longitude),
|
|
82
|
+
city: String(r.city ?? '').trim(),
|
|
83
|
+
state: String(r.state ?? '').trim(),
|
|
84
|
+
full: String(r.full ?? '').trim(),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const byZip = new Map();
|
|
88
|
+
for (const r of list) {
|
|
89
|
+
if (!byZip.has(r.zip)) byZip.set(r.zip, r);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const byCityState = new Map();
|
|
93
|
+
for (const r of list) {
|
|
94
|
+
const key = cityStateKey(r.city, r.state);
|
|
95
|
+
if (!byCityState.has(key)) byCityState.set(key, []);
|
|
96
|
+
byCityState.get(key).push(r);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const byState = new Map();
|
|
100
|
+
for (const r of list) {
|
|
101
|
+
const key = normalizeState(r.state);
|
|
102
|
+
if (!byState.has(key)) byState.set(key, []);
|
|
103
|
+
byState.get(key).push(r);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const byStateAbbr = new Map();
|
|
107
|
+
for (const r of list) {
|
|
108
|
+
const key = normalizeState(r.state);
|
|
109
|
+
if (!byStateAbbr.has(key)) byStateAbbr.set(key, r.full);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { list, byZip, byCityState, byState, byStateAbbr };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a zip locator API. Module initializes with optional system of counting (miles or kilometers; default is miles).
|
|
117
|
+
*
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {boolean} [options.useMiles=true] - If true, distances are in miles; if false, in kilometers.
|
|
120
|
+
* @param {string} [options.dataPath] - Path to zip-codes.json. Defaults to bundled zip-codes.json.
|
|
121
|
+
* @returns {object} API with getZipData, getZipCoordinates, getZipCodes, getClosestByZipCode, getDataByCoordinates, getDistanceBetweenZipCodes
|
|
122
|
+
*/
|
|
123
|
+
function createZipLocator(options = {}) {
|
|
124
|
+
const useMiles = options.useMiles !== false;
|
|
125
|
+
const dataPath = options.dataPath ?? DEFAULT_DATA_PATH;
|
|
126
|
+
const { list, byZip, byCityState, byState, byStateAbbr } = loadZipData(dataPath);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
/**
|
|
130
|
+
* Returns full data for a zip code (state, city, lat & lng). Returns only one entry per zip.
|
|
131
|
+
* @param {string|number} zipCode
|
|
132
|
+
* @returns {object|undefined} { zip, latitude, longitude, city, state, full } or undefined
|
|
133
|
+
*/
|
|
134
|
+
getZipData(zipCode) {
|
|
135
|
+
const zip = normalizeZip(zipCode);
|
|
136
|
+
return byZip.get(zip);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns coordinates (lat & lng) for a zip code.
|
|
141
|
+
* @param {string|number} zipCode
|
|
142
|
+
* @returns {{ latitude: number, longitude: number }|undefined}
|
|
143
|
+
*/
|
|
144
|
+
getZipCoordinates(zipCode) {
|
|
145
|
+
const r = byZip.get(normalizeZip(zipCode));
|
|
146
|
+
return r == null ? undefined : { latitude: r.latitude, longitude: r.longitude };
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns all zip codes and coordinates for a city and state (can return multiple entries).
|
|
151
|
+
* @param {string} city
|
|
152
|
+
* @param {string} state
|
|
153
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full }>}
|
|
154
|
+
*/
|
|
155
|
+
getZipCodes(city, state) {
|
|
156
|
+
const key = cityStateKey(city, state);
|
|
157
|
+
return byCityState.get(key) ?? [];
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Returns zips within radius of the given zip, with distance. Radius and returned distance use miles or km based on useMiles.
|
|
162
|
+
* @param {string|number} zipCode - Center zip
|
|
163
|
+
* @param {number} radius - Radius in miles (if useMiles true) or kilometers (if false)
|
|
164
|
+
* @param {boolean} [useMilesOverride] - Override instance default for this call (true = miles, false = km)
|
|
165
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full, distance }>} distance in miles or km
|
|
166
|
+
*/
|
|
167
|
+
getClosestByZipCode(zipCode, radius, useMilesOverride) {
|
|
168
|
+
const inMiles = useMilesOverride !== undefined ? useMilesOverride : useMiles;
|
|
169
|
+
const center = byZip.get(normalizeZip(zipCode));
|
|
170
|
+
if (!center) return [];
|
|
171
|
+
const results = [];
|
|
172
|
+
for (const r of list) {
|
|
173
|
+
const d = haversineDistance(center.latitude, center.longitude, r.latitude, r.longitude, inMiles);
|
|
174
|
+
if (d <= radius) results.push({ ...r, distance: d });
|
|
175
|
+
}
|
|
176
|
+
results.sort((a, b) => a.distance - b.distance);
|
|
177
|
+
return results;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns zip records with distance from (lat, lng), sorted by option. Distance unit follows instance setting.
|
|
182
|
+
* @param {number} lat - Latitude
|
|
183
|
+
* @param {number} lng - Longitude
|
|
184
|
+
* @param {object} [options] - { sort: 'distance' | 'zip' | 'city' } (default: 'distance')
|
|
185
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full, distance }>}
|
|
186
|
+
*/
|
|
187
|
+
getDataByCoordinates(lat, lng, options = {}) {
|
|
188
|
+
const sort = options.sort ?? 'distance';
|
|
189
|
+
const withDistance = list.map((r) => ({
|
|
190
|
+
...r,
|
|
191
|
+
distance: haversineDistance(lat, lng, r.latitude, r.longitude, useMiles),
|
|
192
|
+
}));
|
|
193
|
+
if (sort === 'distance') {
|
|
194
|
+
withDistance.sort((a, b) => a.distance - b.distance);
|
|
195
|
+
} else if (sort === 'zip') {
|
|
196
|
+
withDistance.sort((a, b) => a.zip.localeCompare(b.zip));
|
|
197
|
+
} else if (sort === 'city') {
|
|
198
|
+
withDistance.sort((a, b) =>
|
|
199
|
+
a.city.localeCompare(b.city) || a.state.localeCompare(b.state)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return withDistance;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Returns distance between two zip codes.
|
|
207
|
+
* @param {string|number} zip1
|
|
208
|
+
* @param {string|number} zip2
|
|
209
|
+
* @param {boolean} [useMilesOverride] - true = miles, false = km (default: use instance setting)
|
|
210
|
+
* @returns {number|undefined} Distance in miles or km, or undefined if either zip not found
|
|
211
|
+
*/
|
|
212
|
+
getDistanceBetweenZipCodes(zip1, zip2, useMilesOverride) {
|
|
213
|
+
const inMiles = useMilesOverride !== undefined ? useMilesOverride : useMiles;
|
|
214
|
+
const a = byZip.get(normalizeZip(zip1));
|
|
215
|
+
const b = byZip.get(normalizeZip(zip2));
|
|
216
|
+
if (!a || !b) return undefined;
|
|
217
|
+
return haversineDistance(a.latitude, a.longitude, b.latitude, b.longitude, inMiles);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Returns all zip codes (with coordinates) in a state.
|
|
222
|
+
* @param {string} state - Two-letter state abbreviation (e.g. 'AL', 'NY')
|
|
223
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full }>}
|
|
224
|
+
*/
|
|
225
|
+
getZipsInState(state) {
|
|
226
|
+
const key = normalizeState(state);
|
|
227
|
+
return byState.get(key) ?? [];
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns unique city names in a state (sorted).
|
|
232
|
+
* @param {string} state - Two-letter state abbreviation
|
|
233
|
+
* @returns {string[]}
|
|
234
|
+
*/
|
|
235
|
+
getCitiesInState(state) {
|
|
236
|
+
const key = normalizeState(state);
|
|
237
|
+
const records = byState.get(key) ?? [];
|
|
238
|
+
const cities = [...new Set(records.map((r) => r.city).filter(Boolean))];
|
|
239
|
+
return cities.sort((a, b) => a.localeCompare(b));
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns true if the zip code exists in the dataset.
|
|
244
|
+
* @param {string|number} zipCode
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
isValidZip(zipCode) {
|
|
248
|
+
return byZip.has(normalizeZip(zipCode));
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Returns zip records whose zip code starts with the given prefix (e.g. '100' for NYC area).
|
|
253
|
+
* @param {string} prefix - Zip prefix (e.g. '100', '350')
|
|
254
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full }>}
|
|
255
|
+
*/
|
|
256
|
+
searchZipPrefix(prefix) {
|
|
257
|
+
const p = String(prefix).trim();
|
|
258
|
+
if (!p) return [];
|
|
259
|
+
return list.filter((r) => r.zip.startsWith(p));
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Returns zip records inside the given bounding box (inclusive).
|
|
264
|
+
* @param {number} minLat - Minimum latitude
|
|
265
|
+
* @param {number} maxLat - Maximum latitude
|
|
266
|
+
* @param {number} minLng - Minimum longitude
|
|
267
|
+
* @param {number} maxLng - Maximum longitude
|
|
268
|
+
* @returns {Array<{ zip, latitude, longitude, city, state, full }>}
|
|
269
|
+
*/
|
|
270
|
+
getZipsInBoundingBox(minLat, maxLat, minLng, maxLng) {
|
|
271
|
+
const latMin = Math.min(minLat, maxLat);
|
|
272
|
+
const latMax = Math.max(minLat, maxLat);
|
|
273
|
+
const lngMin = Math.min(minLng, maxLng);
|
|
274
|
+
const lngMax = Math.max(minLng, maxLng);
|
|
275
|
+
return list.filter(
|
|
276
|
+
(r) =>
|
|
277
|
+
r.latitude >= latMin &&
|
|
278
|
+
r.latitude <= latMax &&
|
|
279
|
+
r.longitude >= lngMin &&
|
|
280
|
+
r.longitude <= lngMax
|
|
281
|
+
);
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Returns distances from an origin zip to multiple target zips in one call.
|
|
286
|
+
* @param {string|number} originZip - Origin zip code
|
|
287
|
+
* @param {Array<string|number>} zipCodes - Target zip codes
|
|
288
|
+
* @param {boolean} [useMilesOverride] - true = miles, false = km (default: instance setting)
|
|
289
|
+
* @returns {Array<{ zip: string, distance: number|undefined }>} Same order as zipCodes; distance undefined if zip not found
|
|
290
|
+
*/
|
|
291
|
+
getDistancesFromZip(originZip, zipCodes, useMilesOverride) {
|
|
292
|
+
const inMiles = useMilesOverride !== undefined ? useMilesOverride : useMiles;
|
|
293
|
+
const origin = byZip.get(normalizeZip(originZip));
|
|
294
|
+
if (!origin) return zipCodes.map((z) => ({ zip: normalizeZip(z), distance: undefined }));
|
|
295
|
+
return zipCodes.map((z) => {
|
|
296
|
+
const zip = normalizeZip(z);
|
|
297
|
+
const target = byZip.get(zip);
|
|
298
|
+
const distance =
|
|
299
|
+
target == null
|
|
300
|
+
? undefined
|
|
301
|
+
: haversineDistance(
|
|
302
|
+
origin.latitude,
|
|
303
|
+
origin.longitude,
|
|
304
|
+
target.latitude,
|
|
305
|
+
target.longitude,
|
|
306
|
+
inMiles
|
|
307
|
+
);
|
|
308
|
+
return { zip, distance };
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns city and state for a zip code (for display).
|
|
314
|
+
* @param {string|number} zipCode
|
|
315
|
+
* @returns {{ city: string, state: string, formatted: string }|undefined} formatted is "City, ST"
|
|
316
|
+
*/
|
|
317
|
+
getCityState(zipCode) {
|
|
318
|
+
const r = byZip.get(normalizeZip(zipCode));
|
|
319
|
+
if (!r) return undefined;
|
|
320
|
+
return {
|
|
321
|
+
city: r.city,
|
|
322
|
+
state: r.state,
|
|
323
|
+
formatted: `${r.city}, ${r.state}`,
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Returns full state name for a two-letter abbreviation (e.g. 'AL' -> 'Alabama').
|
|
329
|
+
* @param {string} stateAbbr - Two-letter state code
|
|
330
|
+
* @returns {string|undefined}
|
|
331
|
+
*/
|
|
332
|
+
getFullStateName(stateAbbr) {
|
|
333
|
+
const key = normalizeState(stateAbbr);
|
|
334
|
+
return byStateAbbr.get(key);
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Default instance (miles) for backward compatibility and simple use
|
|
340
|
+
const defaultInstance = createZipLocator();
|
|
341
|
+
|
|
342
|
+
module.exports = createZipLocator;
|
|
343
|
+
module.exports.createZipLocator = createZipLocator;
|
|
344
|
+
module.exports.loadZipData = loadZipData;
|
|
345
|
+
module.exports.normalizeZip = normalizeZip;
|
|
346
|
+
module.exports.distanceMiles = distanceMiles;
|
|
347
|
+
module.exports.haversineDistance = haversineDistance;
|
|
348
|
+
// Attach default API to module for require('js-zip-code-locator').getZipData(...)
|
|
349
|
+
Object.assign(module.exports, defaultInstance);
|
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "js-zip-code-locator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Node.js module for ZIP code lookups: data, coordinates, by city/state, closest by radius, by coordinates",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "node --test test/"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["zip", "zipcode", "coordinates", "geolocation", "usa"],
|
|
10
|
+
"license": "MIT"
|
|
11
|
+
}
|