npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

pnpm add @forwardslashns/fws-geo-location-api

Prerequisites

This library authenticates against the GeoNames free API using account usernames, not API keys.

  1. Register one or more free accounts at https://www.geonames.org/login
  2. Enable the free web services for each account in your profile settings
  3. Pass the usernames in the usernames option 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); // boolean

Region 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 getPostalCodesPage when 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); // null

Types

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-list

Username 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.js

Published to the public npm registry under the @forwardslashns scope.


License

ISC — ForwardSlash d.o.o.