@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
pnpm add @studnicky/geo-adaptersRequires @studnicky:registry=https://npm.pkg.github.com in .npmrc.
Offline country lookup
CountryCoderAdapter resolves country/region boundaries entirely offline, with no network call:
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:
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:
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 };
}