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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.{yml,yaml}]
indent_size = 2
6 changes: 1 addition & 5 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ module.exports = {
commonjs: true,
node: true,
},
extends: ['@parcellab/eslint-config/base'],
extends: ['@parcellab/eslint-config/base', 'prettier'],
rules: {
'comma-dangle': ['error', 'always-multiline'],
'prefer-const': 'error',
'quote-props': ['error', 'consistent-as-needed'],
'quotes': ['error', 'single'],
'semi': ['error', 'always'],
'unicorn/prefer-module': 'off',
},
};
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run format:check
- run: npm run build --if-present
- run: npm test
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
package-lock.json

# Generated/large data files: keep existing formatting to avoid noisy diffs.
country/**/*.json
regionNames/**/*.json
regions/**/*.json
Comment thread
ilin-andrey marked this conversation as resolved.
15 changes: 15 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"quoteProps": "consistent",
"printWidth": 100,
"overrides": [
{
"files": ["*.yml", "*.yaml"],
"options": {
"singleQuote": false
}
}
]
}
13 changes: 13 additions & 0 deletions DATA_REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# Country Data Requirements

Guide for adding a new country to the static Region Identifier dataset. Each supported country needs three JSON files plus a small code change.

## 1) Country config – `country/ISO3.json`

- Keys:
- `zipCodeFormat`: `"numeric"` or `"alpha"`.
- `zipCodeLength`: integer, required for numeric formats to pad/compare values (USA=5, AUS=4, DEU=5, etc.). Omit for alpha formats (CAN/GBR/NLD).
- Example:

```json
{ "zipCodeFormat": "numeric", "zipCodeLength": 5 }
```

## 2) Region names – `regionNames/ISO3.json`

- Plain object mapping ISO 3166-2 region codes (same codes used in `regions/*.json`) to display names in English.
- Example (`regionNames/AUS.json`):

```json
{
"AU-NSW": "New South Wales",
Expand All @@ -21,11 +26,15 @@ Guide for adding a new country to the static Region Identifier dataset. Each sup
```

## 3) Region mappings – `regions/ISO3.json`

Array of objects that map postal codes to ISO 3166-2 region codes. Formats already used in the repo:

- **Numeric intervals** (inclusive): objects with `low`, `high`, `region`. Values can be numbers or numeric strings if leading zeros matter. Example (`regions/AUS.json`):

```json
{ "region": "AU-NSW", "low": 1000, "high": 1999 }
```

- **Explicit codes**: objects with `low`, `region`, no `high`. Every postal code (or prefix) must be listed. Use strings to preserve zeros. Examples:
- Belgium lists every 4-digit code (`regions/BEL.json`).
- Spain lists 5-digit codes as strings (`regions/ESP.json`).
Expand All @@ -34,20 +43,24 @@ Array of objects that map postal codes to ISO 3166-2 region codes. Formats alrea
- **Discrete lists**: objects with `region` and `list` (array of codes). Numeric countries store numbers; matching uses `zipCodeLength` to pad leading zeros before comparison. Examples: USA (`list` of 5-digit numbers), RUS (per-region lists).

Additional format notes:

- Files can mix formats if needed. Order matters because the lookup stops at the first match.
- Intervals and lists are treated as inclusive ranges; for alpha codes only exact equality is used.
- Keep codes as strings when leading zeros are significant.

## 4) Code wiring

- Add the ISO3 code to `availableCountries` in `lib/region.js`; otherwise the static data is ignored and Google is used.
- If the country needs custom postal-code normalization, extend `validateZipCode` (see existing cases for GBR, CAN, NLD, MEX).

## 5) Testing

- Add test cases to `test/tests.js` under `countriesPostalCodes`: include the country ISO/name, a representative zip, expected region code, and set `usingGoogle: false`.
- Run `npm test` to execute the Node.js test runner.
- Optional: verify pretty-name lookups via `getNameFromCountryAndRegion` using the `regionNames` file.

## 6) Data quality checklist

- Region codes must be valid ISO 3166-2 for the country.
- `regionNames` should be English and cover every region code present in `regions/ISO3.json`.
- Postal-code coverage should include territories and edge prefixes (e.g., US territories, Canadian northern prefixes).
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

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.

*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)
_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)

#### Predefined Regions

- AUS
- AUT
- BRA
Expand Down Expand Up @@ -39,22 +41,27 @@ Utility module that provides an easy way to identify the region of the country d
- USA

## Test

```sh
$ npm test
```

### License

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/.

## Usage

#### Basic:

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

#### Get region:

```javascript
//Using country name
identifier.get('Deutschland', '6578')
Expand Down
48 changes: 19 additions & 29 deletions lib/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,19 +172,13 @@ function getRegionByZipCodeOnStaticFiles(countryIso, zip) {
(regionMapping.list.includes(parseInt(zip)) ||
(countryConfig.zipCodeFormat === 'numeric'
? regionMapping.list.some((l) =>
zip.startsWith(
String(l).padStart(countryConfig.zipCodeLength, '0'),
),
zip.startsWith(String(l).padStart(countryConfig.zipCodeLength, '0')),
)
: false))
) {
return regionMapping.region;
} else if (!regionMapping.high && zip === regionMapping.low)
return regionMapping.region;
else if (
parseInt(zip) >= regionMapping.low &&
parseInt(zip) <= regionMapping.high
)
} else if (!regionMapping.high && zip === regionMapping.low) return regionMapping.region;
else if (parseInt(zip) >= regionMapping.low && parseInt(zip) <= regionMapping.high)
return regionMapping.region;
}
}
Expand Down Expand Up @@ -293,34 +287,30 @@ class RegionIdentifier {
let response;

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

if (!response?.data) {
return null;
}
if (!response?.data) {
return null;
}

if (response.data?.results?.length <= 0) {
return null;
}
if (response.data?.results?.length <= 0) {
return null;
}

// find country inside of the nested array
const region = response.data.results[0].address_components.find((component) => {
const typeFound = component.types.find(
(type) => type === 'administrative_area_level_1',
);
return typeFound && typeFound.length > 0;
});
// find country inside of the nested array
const region = response.data.results[0].address_components.find((component) => {
const typeFound = component.types.find((type) => type === 'administrative_area_level_1');
return typeFound && typeFound.length > 0;
});

if (!region) {
throw new GoogleMapsAPIError('NOT FOUND');
}
if (!region) {
throw new GoogleMapsAPIError('NOT FOUND');
}

return `${countryIso.alpha2}-${cleanAndCatchFaultyRegionName(region)}`;
return `${countryIso.alpha2}-${cleanAndCatchFaultyRegionName(region)}`;
}
}

Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"homepage": "https://github.com/parcelLab/regionIdentifier",
"scripts": {
"test": "node --test test/tests.js",
"lint": "eslint ."
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"engines": {
"node": ">=24.0.0"
Expand All @@ -30,8 +32,10 @@
},
"devDependencies": {
"@parcellab/eslint-config": "^0.5.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-promise": "^7.3.0"
"eslint-plugin-promise": "^7.3.0",
"prettier": "^3.9.4"
},
"overrides": {
"eslint-plugin-unicorn": "47.0.0",
Expand Down
2 changes: 1 addition & 1 deletion test/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ identifier
.catch((err) => {
console.error(err);

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