Skip to content
Merged

Dev #71

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
5 changes: 5 additions & 0 deletions .changeset/add-city-pagination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add `City.getCitiesPaginated({ countryCode?, stateCode?, page?, pageSize? })` returning `{ items, page, pageSize, total, hasMore }` — a paged slice for browser/memory-constrained environments instead of loading all ~148k cities at once. Pass `countryCode`/`stateCode` to scope before paginating. Also available as a `CountryHelper` instance method.
5 changes: 5 additions & 0 deletions .changeset/add-cli-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add a `countrydata-export` CLI (via `npx countrydata-export`) that dumps countries/states/cities as CSV or SQL for non-JS consumers: `--entity <countries|states|cities>`, `--format csv|sql`, `--out <path>`. No new dependency — pure Node built-ins.
5 changes: 5 additions & 0 deletions .changeset/add-cross-entity-filters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add cross-entity country filters: `getCountriesByCurrency(code)`, `getCountriesByContinent(name)`, `getCountriesByLanguage(name)`, and `getCountriesByRegion(name)` (matches a country's administrative region, e.g. `"Bavaria"` → Germany). All case-insensitive, all O(1)/O(n) lookups backed by Maps built at load time. Also available as `CountryHelper` instance methods.
5 changes: 5 additions & 0 deletions .changeset/add-geo-utilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add geo distance utilities: `Geo.haversineDistanceKm(lat1, lon1, lat2, lon2)`, `Geo.getNearestCountry(lat, lon)`, and `Geo.getNearestCity(lat, lon, options?)` (accepts `{ countryCode, stateCode }` to scope the search instead of scanning all ~148k cities). Also available as `CountryHelper` instance methods. No new dependency.
5 changes: 5 additions & 0 deletions .changeset/add-i18n-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add translated country names: `Country.getCountryName(code, locale?)` falls back to the English `countryName` when the locale is omitted or has no translation. Adds a new optional `translations?: Record<string, string>` field to `ICountry` covering `ar`, `zh`, `fr`, `ru`, `es`, `pt`, `de` (99-100% coverage across all 250 countries), sourced from Wikidata (CC0) via `scripts/fetch-wikidata.cjs`. Also available as a `CountryHelper` instance method.
5 changes: 5 additions & 0 deletions .changeset/add-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add `Search.searchCountries()`, `Search.searchStates()`, and `Search.searchCities()` — case-insensitive and diacritic-insensitive substring search (e.g. `"sao paulo"` matches `"São Paulo"`), with exact/prefix matches ranked first. Accepts an optional `{ limit }` to cap result count. Also available as `CountryHelper` instance methods. No new dependency.
5 changes: 5 additions & 0 deletions .changeset/add-validation-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add validation helpers: `Country.isValidCountryCode(code)`, `Country.isValidPhoneCode(code)`, and `State.isValidStateCode(stateCode, countryCode)`. Also available as `CountryHelper` instance methods.
5 changes: 5 additions & 0 deletions .changeset/enrich-country-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"countrydata.js": minor
---

Add capital, population, area, continent, official/native name, demonym, languages, borders, TLDs, UN membership/independence flags, and currency name/symbol to every country record. Sourced from Wikidata (CC0) and a hand-maintained ISO 4217 currency table via a new one-time build script (`npm run build:data:fetch`) — no runtime dependency added, the library stays fully offline.
8 changes: 8 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name: "CodeQL config"

# Sample/ holds standalone demo apps (their own package.json/tsconfig, not part of the
# published package) — no need to security-scan throwaway example code.
paths-ignore:
- Sample/**
- coverage/**
- dist/**
11 changes: 8 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ jobs:
with:
languages: javascript-typescript
queries: security-extended

- name: Autobuild
uses: github/codeql-action/autobuild@v4
config-file: ./.github/codeql/codeql-config.yml

# Autobuild's heuristic crawl picks up Sample/TypeScript's own tsconfig.json and tries
# to `tsc` it, which fails because that sample imports from ../../dist/index.js and this
# job never builds the root package. Build explicitly instead — see
# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#specifying-build-commands-for-compiled-languages-in-a-github-actions-workflow
- name: Build
run: npm run build

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,46 @@ jobs:
files: ./coverage/*.json
flags: unittests
name: codecov-umbrella

samples:
name: Verify samples run against the built package
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: 20.x
cache: 'npm'

- name: Install root dependencies
run: npm install

- name: Build the package
run: npm run build

- name: Run JavaScript sample
working-directory: Sample/javascript
run: |
npm install
node app.js

- name: Run TypeScript sample
working-directory: Sample/TypeScript
run: |
npm install
node --loader ts-node/esm index.ts

- name: Run Express sample
working-directory: Sample/express
run: |
npm install
node app.js &
SERVER_PID=$!
sleep 2
curl -sf http://localhost:8000/api/countries > /dev/null
curl -sf http://localhost:8000/api/countries/GH/name/fr | grep -q Ghana
curl -sf "http://localhost:8000/api/search/countries?q=ghan" | grep -q Ghana
kill $SERVER_PID
122 changes: 118 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [![CI](https://github.com/Clifftech123/CountryData.js/actions/workflows/main.yml/badge.svg)](https://github.com/Clifftech123/CountryData.js/actions/workflows/main.yml) | [![codecov](https://codecov.io/github/Clifftech123/CountryData.js/graph/badge.svg?token=42Y3GT9MKN)](https://codecov.io/github/Clifftech123/CountryData.js) | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Clifftech123_CountryData.js&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Clifftech123_CountryData.js) |

[![npm version](https://img.shields.io/npm/v/countrydata.js)](https://www.npmjs.com/package/countrydata.js)
[![NPM Downloads](https://img.shields.io/npm/d18m/countrydata.js)](https://www.npmjs.com/package/countrydata.js)
[![License: MIT](https://img.shields.io/npm/l/countrydata.js)](./LICENSE)

## Features

- **250 countries** name, ISO code, phone code, flag emoji, currency code, latitude/longitude, timezones, and regions
- **250 countries** — name, ISO code, phone code, flag emoji, currency (code/name/symbol),
capital, population, area, continent, native/official name, demonym, languages, bordering
countries, TLD, UN membership, latitude/longitude, timezones, and regions
- **4 963 states & provinces** with ISO codes and coordinates
- **148 000+ cities** linked to their country and state, lazily loaded
- **148 000+ cities** linked to their country and state, lazily loaded, with paginated access
- **Fuzzy search** — case- and diacritic-insensitive (`Search.searchCountries()`, `searchStates()`, `searchCities()`)
- **Cross-entity filters** — by currency, continent, language, or admin region
- **Validation helpers** — `isValidCountryCode()`, `isValidPhoneCode()`, `isValidStateCode()`
- **Geo utilities** — Haversine distance, nearest country/city lookups
- **i18n** — translated country names in 7 locales via `getCountryName(code, locale)`
- **CLI** — `npx countrydata-export` dumps any dataset as CSV or SQL
- Synchronous API with O(1) lookups via Map indexes
- Works in both **ESM** and **CommonJS** environments
- Full **TypeScript** types included
Expand All @@ -30,23 +40,32 @@ npm install countrydata.js
### Module API (recommended)

```typescript
import { Country, State, City, Region, Timezone } from 'countrydata.js';
import { Country, State, City, Region, Timezone, Search, Geo } from 'countrydata.js';

// Countries
const all = Country.getAllCountries();
const us = Country.getCountryByCode('US');
const byPhone = Country.getCountryByPhoneCode('+233');
const flag = Country.getCountryFlag('GH'); // '🇬🇭'
const sorted = Country.sortCountries();
const eurCountries = Country.getCountriesByCurrency('EUR');
const african = Country.getCountriesByContinent('Africa');
const englishSpeaking = Country.getCountriesByLanguage('English');
const owner = Country.getCountriesByRegion('California'); // → [United States]
const validCode = Country.isValidCountryCode('GH'); // true
const validPhone = Country.isValidPhoneCode('+233'); // true
const nameInFrench = Country.getCountryName('GH', 'fr'); // 'Ghana'

// States / Provinces
const states = State.getStatesOfCountry('US');
const ca = State.getStateByCodeAndCountry('CA', 'US');
const sorted = State.sortStates(states);
const validState = State.isValidStateCode('CA', 'US'); // true

// Cities (lazily loaded on first call)
const cities = City.getCitiesOfState('US', 'CA');
const ghCities = City.getCitiesOfCountry('GH');
const page = City.getCitiesPaginated({ countryCode: 'US', page: 1, pageSize: 25 });

// Regions
const regions = Region.getRegionsByCountryCode('US');
Expand All @@ -56,6 +75,15 @@ const region = Region.getRegionByShortCode('US', 'CA');
const tzList = Timezone.getTimezonesByCountryCode('US');
const allTz = Timezone.getAllTimezones();
const countries = Timezone.getCountriesByTimezone('America/New_York');

// Search (case-insensitive, prefix matches ranked first)
const countryMatches = Search.searchCountries('ghan'); // → [Ghana]
const stateMatches = Search.searchStates('accra');
const cityMatches = Search.searchCities('kum');

// Geo (distance in km, nearest lookups)
const distance = Geo.haversineDistanceKm(40.7128, -74.0060, 51.5074, -0.1278);
const nearestCity = Geo.getNearestCity(40.7128, -74.0060, { countryCode: 'US' });
```

### Class API
Expand Down Expand Up @@ -98,6 +126,15 @@ All methods are **synchronous** and return data directly no `await`, no `.then()
| `getCountryByPhoneCode(code)` | `ICountry \| undefined` | Find by phone code (e.g. `"+1"`) |
| `getCountryFlag(code)` | `string` | Emoji flag (e.g. `"🇺🇸"`) |
| `sortCountries(countries?)` | `ICountry[]` | Alphabetical copy (defaults to all) |
| `getCountriesByCurrency(code)` | `ICountry[]` | Countries sharing a currency code (e.g. `"EUR"`) |
| `getCountriesByContinent(name)` | `ICountry[]` | Countries on a continent (e.g. `"Africa"`) |
| `getCountriesByLanguage(name)` | `ICountry[]` | Countries with a given official language |
| `getCountriesByRegion(name)` | `ICountry[]` | Country containing a named admin region (e.g. `"California"`) |
| `isValidCountryCode(code)` | `boolean` | Whether an ISO code exists in the dataset |
| `isValidPhoneCode(code)` | `boolean` | Whether a phone code exists in the dataset |
| `getCountryName(code, locale?)` | `string \| undefined` | Translated name; falls back to English if the locale is missing |

Supported `locale` codes: `ar`, `zh`, `fr`, `ru`, `es`, `pt`, `de` (plus `en`/omitted → `countryName`). Coverage is 99–100% across all 250 countries for every locale. Raw translations are also available on `country.translations` (`Record<string, string>`).

### `State`

Expand All @@ -107,6 +144,7 @@ All methods are **synchronous** and return data directly no `await`, no `.then()
| `getStatesOfCountry(countryCode)` | `IState[]` | States for one country |
| `getStateByCodeAndCountry(stateCode, countryCode)` | `IState \| undefined` | Single state lookup |
| `sortStates(states?)` | `IState[]` | Alphabetical copy (defaults to all) |
| `isValidStateCode(stateCode, countryCode)` | `boolean` | Whether that state exists for that country |

### `City`

Expand All @@ -118,6 +156,7 @@ All methods are **synchronous** and return data directly no `await`, no `.then()
| `getCitiesOfCountry(countryCode)` | `ICity[]` | Cities for one country |
| `getCitiesOfState(countryCode, stateCode)` | `ICity[]` | Cities for one state |
| `sortCities(cities?)` | `ICity[]` | Sorted by country → state → name |
| `getCitiesPaginated(options?)` | `{ items, page, pageSize, total, hasMore }` | Paged slice; pass `countryCode`/`stateCode` to scope before paginating |

### `Region`

Expand All @@ -135,6 +174,36 @@ All methods are **synchronous** and return data directly no `await`, no `.then()
| `getTimezonesByCountryCode(countryCode)` | `ITimezone[]` | Timezones for one country |
| `getCountriesByTimezone(zoneName)` | `ICountry[]` | Countries that observe a given timezone |

### `Search`

Case-insensitive, diacritic-insensitive substring matching (`"sao paulo"` matches `"São Paulo"`), with exact matches ranked first, then prefix matches, then other substring matches. No fuzzy-matching dependency. Pass `{ limit }` to cap the number of results — useful for city search, which can otherwise return thousands of matches.

| Method | Returns | Description |
| ---------------------------------- | ------------ | --------------------------------------- |
| `searchCountries(query, options?)` | `ICountry[]` | Countries whose `countryName` matches |
| `searchStates(query, options?)` | `IState[]` | States whose `name` matches |
| `searchCities(query, options?)` | `ICity[]` | Cities whose `name` matches |

```typescript
Search.searchCities('sao paulo'); // finds "São Paulo" despite no accents in the query
Search.searchCountries('a', { limit: 10 }); // top 10 matches only
```

### `Geo`

Country/state/city coordinates are single points (typically a geographic centroid), not borders — so "nearest country" means nearest *centroid*, which can be counterintuitive for large countries (e.g. a point in New York City is nearer Bermuda's centroid than the continental US's). `getNearestCity()` scans the whole ~148k-city dataset if no `countryCode`/`stateCode` is given (~100ms); pass one to scope the search to an already-indexed subset (~3ms).

| Method | Returns | Description |
| ------------------------------------------------ | --------------------- | --------------------------------------------- |
| `haversineDistanceKm(lat1, lon1, lat2, lon2)` | `number` | Great-circle distance between two coordinates |
| `getNearestCountry(lat, lon)` | `ICountry \| undefined` | Nearest country by centroid distance |
| `getNearestCity(lat, lon, options?)` | `ICity \| undefined` | Nearest city; `options.countryCode`/`stateCode` narrow the search |

```typescript
Geo.haversineDistanceKm(40.7128, -74.0060, 51.5074, -0.1278); // NYC → London, ≈5570 km
Geo.getNearestCity(40.7128, -74.0060, { countryCode: 'US' }); // scoped — fast
```

### `CountryHelper` class

Wraps the module functions above into a class. All module methods are available as instance methods with equivalent names.
Expand All @@ -156,10 +225,25 @@ interface ICountry {
phoneCode: string;
countryFlag: string;
currencyCode?: string;
currencyName?: string;
currencySymbol?: string;
latitude?: string;
longitude?: string;
timezones?: ITimezone[];
regions: IRegion[];
capital?: string;
population?: number;
area?: number;
continent?: string;
officialName?: string;
nativeName?: string;
demonym?: string;
languages?: string[];
borders?: string[];
tld?: string[];
unMember?: boolean;
independent?: boolean;
translations?: Record<string, string>;
getStates?(): IState[];
getCities?(): ICity[];
}
Expand Down Expand Up @@ -207,10 +291,22 @@ const gh = Country.getCountryByCode('GH');
// phoneCode: '+233',
// countryFlag: '🇬🇭',
// currencyCode: 'GHS',
// currencyName: 'Ghanaian Cedi',
// currencySymbol: '₵',
// latitude: '8.00000000',
// longitude: '-2.00000000',
// timezones: [{ zoneName: 'Africa/Accra', gmtOffset: 0, ... }],
// regions: [...]
// regions: [...],
// capital: 'Accra',
// population: 32833031,
// area: 238535,
// continent: 'Africa',
// demonym: 'Ghanaian',
// languages: ['English'],
// borders: ['TG', 'BF', 'CI'],
// tld: ['.gh'],
// unMember: true,
// independent: true
// }
```

Expand Down Expand Up @@ -245,6 +341,24 @@ app.listen(3000);

See the [Sample](https://github.com/Clifftech123/CountryData.js/tree/main/Sample) folder for more complete examples.

## CLI

For non-JS consumers, `countrydata-export` dumps any of the three datasets as CSV or SQL:

```bash
npx countrydata-export --entity countries --format csv --out countries.csv
npx countrydata-export --entity cities --format sql > cities.sql
npx countrydata-export --help
```

| Flag | Required | Description |
| ----------- | -------- | ----------------------------------------------- |
| `--entity` | yes | `countries`, `states`, or `cities` |
| `--format` | no | `csv` (default) or `sql` |
| `--out` | no | Output file path (defaults to stdout) |

Nested fields (`timezones`, `regions`) are dropped from the export since they don't fit a flat row; array fields (`languages`, `borders`, `tld`) are joined with `|`.

## Contributing

Contributions are welcome! The data lives in three flat JSON files in the `data/` folder:
Expand Down
Loading