Skip to content

@studnicky/geo-adapters

Source-fetching adapters for external geo and administrative-boundary data (Natural Earth, US Census TIGER, IANA tzdata, country-coder).

Each adapter implements a shared contract (SourceAdapterInterface or AdministrativeSystemAdapterInterface) around one external geo data publisher, normalizing its fetch mechanics, license, and refresh cadence behind a common shape. Network-backed adapters (NaturalEarthAdapter, USCensusTigerAdapter) support opt-in on-disk response caching via AdapterDiskCache; offline adapters (CountryCoderAdapter, IANATzdataAdapter) resolve directly from a bundled dataset.

Install

bash
pnpm add @studnicky/geo-adapters

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Offline country lookup

CountryCoderAdapter resolves country/region boundaries entirely offline, with no network call:

ts
import { CountryCoderAdapter } from '../src/index.js';

async function demonstrateCountryCoderLookup(): Promise<{ 'franceFeatureCount': number; 'franceFeaturesAllMatchIso': boolean; 'sourceId': string; 'totalFeatureCount': number }> {
  const adapter = CountryCoderAdapter.create();
  console.log('sourceId:', adapter.sourceId);
  console.log('license:', adapter.license);
  console.log('refreshPolicy:', adapter.refreshPolicy);

  // fetch() is fully offline — no network call — and resolves the bundled dataset
  const all = await adapter.fetch({});
  console.log('total bundled features:', all.data.features.length);

  // pass 'region' to narrow to features matching an ISO 3166-1 alpha-2 code
  const france = await adapter.fetch({ 'region': ['FR'] });
  console.log('FR features:', france.data.features.length);

  return {
    'franceFeatureCount':        france.data.features.length,
    'franceFeaturesAllMatchIso': france.data.features.every((feature) => {
      return feature.properties?.iso1A2 === 'FR';
    }),
    'sourceId':         adapter.sourceId,
    'totalFeatureCount': all.data.features.length
  };
}

Offline timezone lookup

IANATzdataAdapter resolves a coordinate to its IANA timezone, also fully offline:

ts
import { IANATzdataAdapter } from '../src/index.js';

async function demonstrateIanaTimezoneLookup(): Promise<{ 'missingCoordinatesMessage': string; 'newYorkTimezone': string; 'tokyoTimezone': string }> {
  const adapter = IANATzdataAdapter.create();
  console.log('sourceId:', adapter.sourceId);
  console.log('refreshPolicy:', adapter.refreshPolicy);

  // fetch() is fully offline — no network call — and requires both lat and lon
  const newYork = await adapter.fetch({ 'lat': 40.7128, 'lon': -74.006 });
  console.log('New York timezone:', newYork.data.timezone);

  const tokyo = await adapter.fetch({ 'lat': 35.6812, 'lon': 139.7671 });
  console.log('Tokyo timezone:', tokyo.data.timezone);

  // missing lat/lon rejects rather than resolving a nonsensical result
  let missingCoordinatesMessage = '';
  try {
    await adapter.fetch({});
  }
  catch (error) {
    missingCoordinatesMessage = (error as Error).message;
    console.log('missing coordinates rejects:', missingCoordinatesMessage);
  }

  return {
    'missingCoordinatesMessage': missingCoordinatesMessage,
    'newYorkTimezone':           newYork.data.timezone,
    'tokyoTimezone':             tokyo.data.timezone
  };
}

Disk cache

AdapterDiskCache backs the network-facing adapters' opt-in response caching, keyed so cache hits and misses behave predictably across repeated fetches:

ts
import { AdapterDiskCache } from '../src/index.js';

async function demonstrateAdapterDiskCache(): Promise<{ 'cacheDir': string; 'first': string; 'loadCallCount': number; 'second': string }> {
  const cacheDir = mkdtempSync(join(tmpdir(), 'geo-adapters-example-'));
  const cache = AdapterDiskCache.create(cacheDir);

  // a fake 'load' stands in for a real adapter's network fetch — cache doesn't
  // care where the body came from, only how to key and persist it
  let loadCallCount = 0;
  const load = (): Promise<string> => {
    loadCallCount += 1;
    return Promise.resolve('{"type":"FeatureCollection","features":[]}');
  };

  // first call is a cache miss — load runs and the body is written to disk
  const first = await cache.fetch('natural-earth-lakes', 'static', load);
  console.log('first fetch body:', first);
  console.log('load calls after first fetch:', loadCallCount);

  // second call with the same key is a cache hit — load does not run again
  const second = await cache.fetch('natural-earth-lakes', 'static', load);
  console.log('second fetch body:', second);
  console.log('load calls after second fetch:', loadCallCount);

  return { 'cacheDir': cacheDir, 'first': first, 'loadCallCount': loadCallCount, 'second': second };
}

Source on GitHub