Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .eslintrc.js

This file was deleted.

1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run format:check
- run: npm run lint
- run: npm run build --if-present
- run: npm test
104 changes: 58 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Region Identifier

Utility module that provides an easy way to identify the region of the country depending on the postal code, brings a set of determined regions for some of the countries and if it doesn't find a match uses google geolocation API to get the region.
Utility module that identifies a country's region from a postal code. It first uses the static region mappings bundled in this repository. If no static mapping is available or no static match is found, it falls back to the Google Maps Geocoding API.

_Relevant links:_
## Relevant links

- https://en.wikipedia.org/wiki/ISO_3166-2 — codes used for all the regions can be found here
- https://en.wikipedia.org/wiki/Category:Postal_codes_by_country - explanations of postal code structure per country
- https://download.geonames.org/export/zip/ - all the Geoname files for download (see below)
- https://en.wikipedia.org/wiki/ISO_3166-2 — region codes used by this package
- https://en.wikipedia.org/wiki/Category:Postal_codes_by_country postal-code structure by country
- https://download.geonames.org/export/zip/ — GeoNames postal-code files

#### Predefined Regions
## Predefined regions

- AUS
- AUT
Expand Down Expand Up @@ -40,53 +40,65 @@ _Relevant links:_
- TUR
- USA

## Test
## Usage

```sh
$ npm test
```
### Basic setup

### License
```js
const { RegionIdentifier } = require('region_identifier');

This module was built using adapted information from http://download.geonames.org/ that's registered under the **CC BY 3.0** as well as this module.
Link to more information about **CC BY 3.0** http://creativecommons.org/licenses/by/3.0/.
const identifier = new RegionIdentifier('<GOOGLE API KEY>');
```

## Usage
### Get a region

#### Basic:
`get(country, zipCode)` accepts country names, ISO2 codes, or ISO3 codes. It returns a tuple: `[region, googleUsed]`.

```js
const { RegionIdentifier } = require('region_identifier');

```javascript
const { RegionIdentifier } = requrie('regionIdentifier');
const identifier = new RegionIdentifier('<GOOGLE API KEY>');

async function main() {
const [region, googleUsed] = await identifier.get('DEU', '6578');

console.log(region); // DE-TH
console.log(googleUsed); // false when static data matched, true when Google Maps was used
}

main().catch(console.error);
```

Examples:

```js
await identifier.get('Deutschland', '6578'); // ['DE-TH', false]
await identifier.get('DEU', '6578'); // ['DE-TH', false]
await identifier.get('DE', '6578'); // ['DE-TH', false]
```

If the static data does not contain a matching region, `get()` uses Google Maps. In that case `googleUsed` is `true`. Google API request failures are thrown as `GoogleMapsAPIError`.

### Get a region display name

```js
const name = identifier.getNameFromCountryAndRegion('DEU', 'DE-TH');

console.log(name); // Thüringen
```

#### Get region:

```javascript
//Using country name
identifier.get('Deutschland', '6578')
.then(([region, googleUsed])) => {
console.log(region); // null DE-TH
}
.catch((err) => {
console.error(err);
});

//using ISO3 code
identifier.get('DEU', '6578')
.then(([region, googleUsed])) => {
console.log(region); // null DE-TH
}
.catch((err) => {
console.error(err);
});

//using ISO2 code
identifier.get('DE', '6578')
.then(([region, googleUsed])) => {
console.log(region); // null DE-TH
}
.catch((err) => {
console.error(err);
});
## Development

```sh
npm test
npm run lint
npm run format:check
```

`npm run validate:data` checks consistency between `country/*.json`, `regions/*.json`, `regionNames/*.json`, and the statically supported countries in `lib/region.js`.

See [DATA_REQUIREMENTS.md](./DATA_REQUIREMENTS.md) for the country-data format and checklist.

## License

This module was built using adapted information from http://download.geonames.org/, which is registered under **CC BY 3.0**, as is this module. See http://creativecommons.org/licenses/by/3.0/ for more information.
49 changes: 49 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import js from '@eslint/js';
import prettierConfig from 'eslint-config-prettier/flat';
import { flatConfigs as importXConfigs } from 'eslint-plugin-import-x';
import unicorn from 'eslint-plugin-unicorn';
import globals from 'globals';

export default [
{
ignores: ['node_modules/**', 'coverage/**'],
},
js.configs.recommended,
importXConfigs.recommended,

unicorn.configs.recommended,
{
files: ['**/*.{js,cjs}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
'unicorn/catch-error-name': [
'error',
{
ignore: [String.raw`^error\d*$`, String.raw`^err\d*$`],
},
],
'unicorn/import-style': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-for-each': 'off',
'unicorn/no-await-expression-member': 'off',
'unicorn/name-replacements': 'off',
'unicorn/no-null': 'off',
'unicorn/numeric-separators-style': 'off',
'unicorn/prefer-number-coercion': 'off',
'unicorn/prefer-number-properties': 'off',
'unicorn/prefer-spread': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prevent-abbreviations': 'off',

'prefer-const': 'error',
'unicorn/prefer-module': 'off',
},
},
prettierConfig,
];
19 changes: 10 additions & 9 deletions test/resolve.js → examples/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@ const identifier = new RegionIdentifier('<API KEY>');
const country = 'DEU';
const zip = '79761';

identifier
.get(country, zip)
.then(([region, googleUsed]) => {
async function resolve() {
try {
const [region, googleUsed] = await identifier.get(country, zip);

console.log('~~~');
console.log('Got' + (googleUsed ? ' w/ Google' : ''));
console.log(`Got${googleUsed ? ' w/ Google' : ''}`);
console.log(region);
console.log('~~~');
return;
})
// eslint-disable-next-line unicorn/prefer-top-level-await
.catch((err) => {
} catch (err) {
console.error(err);

if (err instanceof GoogleMapsAPIError) {
console.error('Google Maps API error');
}
});
}
}

resolve();
2 changes: 1 addition & 1 deletion lib/geocode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const availableCountries = new Set([
]);

function validateZipCode(zip) {
return zip.split(' ')[0];
return zip.split(' ', 1)[0];
}

/**
Expand Down
87 changes: 48 additions & 39 deletions lib/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,37 @@ const availableCountries = new Set([
* @return {String} ISO alpha3 code of the country
*/
function resolveCountryToISO3(countryInfo) {
if (countryInfo) {
let ci = countryInfo.trim();
let c;

if (ci.toUpperCase() === 'THE UNITED STATES OF AMERICA') ci = 'USA';
if (ci.toUpperCase() === 'AMERICA') ci = 'USA';

if (ci.length === 3) {
try {
c = Country.ISOcodes(ci, 'ISO3');
} catch {
return null;
}
} else if (ci.length === 2) {
try {
c = Country.ISOcodes(ci);
} catch {
return null;
}
} else {
try {
c = Country.ISOcodes(ci, 'name');
} catch {
return null;
}
}

return c || null;
} else return null;
if (!countryInfo) {
return null;
}

let ci = countryInfo.trim();
let c;

if (ci.toUpperCase() === 'THE UNITED STATES OF AMERICA') ci = 'USA';
if (ci.toUpperCase() === 'AMERICA') ci = 'USA';

if (ci.length === 3) {
try {
c = Country.ISOcodes(ci, 'ISO3');
} catch {
return null;
}
} else if (ci.length === 2) {
try {
c = Country.ISOcodes(ci);
} catch {
return null;
}
} else {
try {
c = Country.ISOcodes(ci, 'name');
} catch {
return null;
}
}

return c || null;
}

/////////////////////////
Expand All @@ -90,7 +92,7 @@ function resolveCountryToISO3(countryInfo) {
function validateZipCode(country, zip) {
switch (country) {
case 'GBR': {
return zip.replaceAll(/\d/g, ' ').split(' ')[0].toUpperCase();
return zip.replaceAll(/\d/g, ' ').split(' ', 1)[0].toUpperCase();
}

case 'CAN': {
Expand Down Expand Up @@ -130,7 +132,7 @@ function validateZipCode(country, zip) {
}

case 'NLD': {
return zip.split(' ')[0];
return zip.split(' ', 1)[0];
}

case 'MEX': {
Expand Down Expand Up @@ -177,19 +179,24 @@ function getRegionByZipCodeOnStaticFiles(countryIso, zip) {
: false))
) {
return regionMapping.region;
} else if (!regionMapping.high && zip === regionMapping.low) return regionMapping.region;
else if (parseInt(zip) >= regionMapping.low && parseInt(zip) <= regionMapping.high)
}

if (
(!regionMapping.high && zip === regionMapping.low) ||
(parseInt(zip) >= regionMapping.low && parseInt(zip) <= regionMapping.high)
) {
return regionMapping.region;
}
}
}

function cleanAndCatchFaultyRegionName(region) {
const parsedRegion = region.short_name.toUpperCase().replace('.', '');

if (/england/i.test(parsedRegion)) return 'ENG';
else if (/northern.*ireland/i.test(parsedRegion)) return 'NIR';
else if (/scotland/i.test(parsedRegion)) return 'SCT';
else if (/wales/i.test(parsedRegion)) return 'WLS';
if (/northern.*ireland/i.test(parsedRegion)) return 'NIR';
if (/scotland/i.test(parsedRegion)) return 'SCT';
if (/wales/i.test(parsedRegion)) return 'WLS';

return parsedRegion;
}
Expand Down Expand Up @@ -282,14 +289,16 @@ class RegionIdentifier {
'https://maps.googleapis.com/maps/api/geocode/json' +
'?key=' +
this.apikey +
'&address=####&sensor=true';
'&address=' +
encodeURIComponent(param) +
'&sensor=true';

let response;

try {
response = await axios.get(googleLink.replace('####', encodeURIComponent(param)));
response = await axios.get(googleLink);
} catch (err) {
throw new GoogleMapsAPIError('Google Maps API request failed: ' + JSON.stringify(err));
throw new GoogleMapsAPIError(`Google Maps API request failed: ${JSON.stringify(err)}`);
}

if (!response?.data) {
Expand Down
Loading