@forwardslashns/fws-geo-location-api
v1.0.3
Published
FWS company dedicated TypeScript wrapper around the GeoNames geolocation API. Provides countries, regions, cities, and postal codes with username rotation and restriction support.
Readme
@forwardslashns/fws-geo-location-api
FWS company-dedicated TypeScript wrapper around the GeoNames geolocation service.
Provides strongly-typed access to countries, administrative regions (states/provinces), cities, and postal codes — with automatic username rotation, global restriction support, paginated results, cursor-based chunked iteration, and frontend-friendly display names.
Table of Contents
- Installation
- Prerequisites
- Quick Start
- Constructor
- Methods
- Types
- Constants
- Error Handling
- Global Restrictions
- Username Rotation
- Publishing (FWS internal)
Installation
pnpm add @forwardslashns/fws-geo-location-apiPrerequisites
This library authenticates against the GeoNames free API using account usernames, not API keys.
- Register one or more free accounts at https://www.geonames.org/login
- Enable the free web services for each account in your profile settings
- Pass the usernames in the
usernamesoption when constructing the client
The free tier allows ~20,000 credits per username per day. For high-volume applications register multiple service accounts and pass them all — the client will rotate automatically on rate-limit errors.
Quick Start
import { GeoLocationClient } from '@forwardslashns/fws-geo-location-api';
const client = new GeoLocationClient({
usernames: ['myGeoNamesUser1', 'myGeoNamesUser2'],
});
// All countries
const countries = await client.getCountries();
// European countries only
const europe = await client.getCountries({ continents: ['EU'] });
// States / provinces for Germany
const regions = await client.getRegions('DE');
console.log(regions.data); // Region[]
// Cities in New York state
const cities = await client.getCities('US', { regionCode: 'NY' });
console.log(cities.data); // City[]
// Postal codes starting with "10" in the Netherlands
const postalCodes = await client.getPostalCodes('NL', { postalCodePrefix: '10' });
console.log(postalCodes.data); // PostalCode[]
// Look up a single postal code
const zip = await client.findPostalCode('10001', 'US');
console.log(zip?.displayName); // "10001 — New York City, New York, United States"Constructor
new GeoLocationClient(options)
| Option | Type | Required | Description |
| ------------------------- | ----------------------- | -------- | --------------------------------------------------------------------------------- |
| usernames | [string, ...string[]] | ✅ | One or more GeoNames usernames. TypeScript enforces at least one at compile time. |
| restrictions.continents | ContinentCode[] | — | Global continent filter applied to every call (can be overridden per call). |
| restrictions.countries | string[] | — | Global ISO 3166-1 alpha-2 country code allow-list applied to every call. |
const client = new GeoLocationClient({
usernames: ['user1', 'user2'],
restrictions: {
continents: ['EU'],
},
});Methods
getCountries
getCountries(options?: GetCountriesOptions): Promise<Country[]>Returns all countries (or a filtered subset). Not paginated — the GeoNames country list is small (~250 entries).
| Option | Type | Description |
| -------------- | ----------------- | ------------------------------------------------------------- |
| continents | ContinentCode[] | Filter to specific continents (overrides global restriction). |
| countryCodes | string[] | Filter to specific ISO2 codes (overrides global restriction). |
// All countries
const all = await client.getCountries();
// Only DACH
const dach = await client.getCountries({ countryCodes: ['DE', 'AT', 'CH'] });
// Only Europe
const eu = await client.getCountries({ continents: ['EU'] });Country fields:
| Field | Type | Description |
| --------------- | ---------- | ------------------------------------------- |
| countryCode | string | ISO 3166-1 alpha-2, e.g. "DE" |
| countryName | string | English name, e.g. "Germany" |
| continent | string | Two-letter continent code, e.g. "EU" |
| continentName | string | Continent full name, e.g. "Europe" |
| capital | string | Capital city name |
| currencyCode | string | ISO 4217 currency code, e.g. "EUR" |
| currencyName | string | Currency display name, e.g. "Euro" |
| population | number | Total country population |
| languages | string[] | BCP 47 language codes spoken in the country |
| areaInSqKm | number | Land area in square kilometres |
| geonameId | number | GeoNames unique identifier |
getRegions
getRegions(countryCode: string, options?: GetRegionsOptions): Promise<PaginatedResult<Region>>Returns first-order administrative divisions (states, provinces, prefectures, etc.) for a country.
| Option | Type | Description |
| --------- | -------- | -------------------------------------------------- |
| maxRows | number | Max results (GeoNames cap: 1 000). Default: 1 000. |
| page | number | 1-based page number. Default: 1. |
const result = await client.getRegions('US');
console.log(result.data); // Region[]
console.log(result.total); // total count
console.log(result.hasMore); // booleanRegion fields:
| Field | Type | Description |
| ------------- | -------- | ------------------------------------ |
| geonameId | number | GeoNames unique identifier |
| name | string | Region name, e.g. "New York" |
| code | string | Short admin code, e.g. "NY" |
| isoCode | string | Full ISO code, e.g. "US-NY" |
| countryCode | string | Parent country ISO2 code |
| population | number | Population (0 if unknown) |
| lat | number | Latitude of centroid |
| lng | number | Longitude of centroid |
| featureCode | string | GeoNames feature code, e.g. "ADM1" |
| adminCode1 | string | Raw admin level-1 code from GeoNames |
getCities
getCities(countryCode: string | null, options?: GetCitiesOptions): Promise<PaginatedResult<City>>Returns populated places. Pass null for countryCode to search globally.
| Option | Type | Description |
| ------------ | -------------------------------------------- | -------------------------------------------------- |
| regionCode | string | Filter by admin level-1 code (state/province). |
| searchTerm | string | Filter by place name prefix. |
| maxRows | number | Max results (GeoNames cap: 1 000). Default: 1 000. |
| orderBy | 'population' \| 'elevation' \| 'relevance' | Sort order. Default: 'population'. |
| page | number | 1-based page number. Default: 1. |
// 100 most populous Japanese cities
const result = await client.getCities('JP', { maxRows: 100 });
// Global search — find all cities named "Paris"
const paris = await client.getCities(null, { searchTerm: 'Paris' });
// New York state cities
const nyc = await client.getCities('US', { regionCode: 'NY' });City fields:
| Field | Type | Description |
| ------------- | -------- | ----------------------------------- |
| geonameId | number | GeoNames unique identifier |
| name | string | City name |
| countryCode | string | ISO2 country code |
| adminName1 | string | Region / state name |
| adminCode1 | string | Region short code |
| population | number | Population |
| lat | number | Latitude |
| lng | number | Longitude |
| featureCode | string | GeoNames feature code, e.g. "PPL" |
| featureName | string | Human-readable feature type |
getPostalCodes
getPostalCodes(countryCode: string, options?: GetPostalCodesOptions): Promise<PaginatedResult<PostalCode>>Returns postal codes matching the supplied filter. A filter is required for large countries (US, DE, etc.) due to a GeoNames dataset-size limit — callers that omit a filter for an oversized country receive a descriptive GeoLocationError pointing them to getAllPostalCodes or getPostalCodesPage.
| Option | Type | Description |
| ------------------ | -------- | ------------------------------------------------------- |
| postalCodePrefix | string | Return codes whose postal code starts with this string. |
| placeName | string | Return codes whose place name starts with this string. |
| maxRows | number | Max results (GeoNames cap: 1 000). Default: 100. |
| page | number | 1-based page number. Default: 1. |
// ZIP codes in the Netherlands starting with "10"
const nld = await client.getPostalCodes('NL', { postalCodePrefix: '10' });
// US codes filtered by city name
const nyZips = await client.getPostalCodes('US', { placeName: 'New York', maxRows: 20 });Each result has a displayName formatted for dropdowns:
"10001 — New York City, New York, United States"getPostalCodesPage
getPostalCodesPage(countryCode: string, options?: GetPostalCodePageOptions): Promise<PostalCodePage>Cursor-based paginated iteration over the entire postal code dataset of a country, including large countries like the US. Internally iterates 36 alphanumeric prefix buckets (0–9, A–Z) so every call works regardless of country size.
Typical cost: 1 GeoNames API request per call (occasionally 2 at a prefix bucket boundary).
| Option | Type | Description |
| --------- | -------- | -------------------------------------------------------------------------------------- |
| cursor | string | Opaque cursor returned by the previous call. Omit to start from the first postal code. |
| maxRows | number | Records per page (capped at 500). Default: 100. |
Returns PostalCodePage:
| Field | Type | Description |
| ---------------- | ----------------------------- | ------------------------------------------------------------------ |
| data | PostalCode[] | Postal codes for this page. |
| nextCursor | string \| null | Pass to the next call to get the following page. null when done. |
| hasMore | boolean | Convenience alias: true when nextCursor is not null. |
| prefixProgress | { done: number; total: 36 } | How many of the 36 prefix buckets have been fully processed. |
// Iterate all US postal codes in pages of 200
let page = await client.getPostalCodesPage('US', { maxRows: 200 });
while (true) {
for (const zip of page.data) {
console.log(zip.postalCode, zip.placeName);
}
if (!page.hasMore) break;
page = await client.getPostalCodesPage('US', {
cursor: page.nextCursor!,
maxRows: 200,
});
}Tip: Store the cursor externally to resume across sessions or HTTP requests.
getAllPostalCodes
getAllPostalCodes(countryCode: string, options?: GetAllPostalCodesOptions): Promise<PostalCode[]>Fetches every postal code for a country in a single call by iterating all 36 prefix buckets internally. Results are deduplicated and returned sorted by postal code. For the US (~43 000 codes) this makes ~80 GeoNames requests.
| Option | Type | Description |
| ------------ | --------------------------------------------- | -------------------------------------------------------------------------------- |
| onProgress | (loaded: ReadonlyArray<PostalCode>) => void | Called after each prefix bucket is fetched. Use to stream progress into your UI. |
const allCodes = await client.getAllPostalCodes('DE', {
onProgress: (loaded) => {
console.log(`${loaded.length} codes loaded so far…`);
},
});
console.log(`Total: ${allCodes.length}`);Use this method when you need the complete dataset in memory (e.g. to build a local search index). Use
getPostalCodesPagewhen you only need a few pages at a time.
findPostalCode
findPostalCode(postalCode: string, countryCode: string): Promise<PostalCode | null>Looks up a single postal code by its exact string. Returns null when not found. Uses exactly 1 GeoNames API credit.
const result = await client.findPostalCode('SW1A 1AA', 'GB');
if (result) {
console.log(result.displayName);
// "SW1A 1AA — Westminster, England, United Kingdom"
}
const missing = await client.findPostalCode('00000', 'US');
console.log(missing); // nullTypes
PaginatedResult<T>
Returned by getRegions, getCities, and getPostalCodes.
interface PaginatedResult<T> {
data: T[];
total: number; // -1 when GeoNames does not return an exact count
page: number; // 1-based
pageSize: number;
hasMore: boolean;
}PostalCode
interface PostalCode {
postalCode: string;
placeName: string;
countryCode: string;
countryName: string;
adminName1: string; // state / region
adminCode1: string;
adminName2: string; // district / county
adminCode2: string;
lat: number;
lng: number;
/** Pre-formatted label: "10001 — New York City, New York, United States" */
displayName: string;
}PostalCodePage
Returned by getPostalCodesPage.
interface PostalCodePage {
data: PostalCode[];
nextCursor: string | null;
hasMore: boolean;
prefixProgress: { done: number; total: 36 };
}AtLeastOne<T>
Utility type enforcing a non-empty array at compile time.
type AtLeastOne<T> = [T, ...T[]];Constants
Continent codes
import { CONTINENT_CODES, CONTINENT_NAMES } from '@forwardslashns/fws-geo-location-api';
CONTINENT_CODES.AFRICA; // 'AF'
CONTINENT_CODES.ANTARCTICA; // 'AN'
CONTINENT_CODES.ASIA; // 'AS'
CONTINENT_CODES.EUROPE; // 'EU'
CONTINENT_CODES.NORTH_AMERICA; // 'NA'
CONTINENT_CODES.OCEANIA; // 'OC'
CONTINENT_CODES.SOUTH_AMERICA; // 'SA'
CONTINENT_NAMES['EU']; // 'Europe'
CONTINENT_NAMES['NA']; // 'North America'Error Handling
import { GeoLocationClient, GeoLocationError } from '@forwardslashns/fws-geo-location-api';
try {
const countries = await client.getCountries();
} catch (err) {
if (err instanceof GeoLocationError) {
console.error(err.message); // human-readable description
console.error(err.code); // GeoNames numeric status code, if applicable
}
}Common error scenarios
| Scenario | Behaviour |
| ------------------------------------------------ | ------------------------------------------------------------------------------------ |
| getPostalCodes without filter on large country | Throws GeoLocationError explaining the limit and recommending getPostalCodesPage |
| All usernames exhausted (rate-limited) | Throws GeoLocationError asking to add usernames or wait for credit reset |
| Invalid / empty countryCode | Throws GeoLocationError immediately (no network call made) |
| GeoNames returns status for no results | Returns empty PaginatedResult (not an error) |
Global Restrictions
const client = new GeoLocationClient({
usernames: ['myUser'],
restrictions: {
continents: ['EU'], // only European data
countries: ['DE', 'AT', 'CH'], // only DACH countries
},
});
// All calls are automatically filtered by the restrictions
const countries = await client.getCountries(); // returns only DE, AT, CH
const cities = await client.getCities('FR'); // returns nothing — FR not in allow-listUsername Rotation
When a username exceeds its daily/hourly/weekly credit limit (GeoNames status codes 17–20) the client automatically advances to the next username in the array. The current call is transparently retried with the new username.
If all usernames are exhausted a GeoLocationError is thrown with a clear message.
const client = new GeoLocationClient({
// Rotate across five accounts for high-volume usage
usernames: ['account1', 'account2', 'account3', 'account4', 'account5'],
});Publishing (FWS internal)
# Create .env with your npm token
echo "NPM_TOKEN=your_token_here" > .env
# Bump patch version, build, and publish
node ./publish.jsPublished to the public npm registry under the @forwardslashns scope.
License
ISC — ForwardSlash d.o.o.
