Typescript port of PyGADM
Go to file
2026-03-21 14:14:46 +01:00
cache/gadm boundary cache 2026-03-21 14:14:46 +01:00
data Typescript Port - Init 2026-03-21 10:52:03 +01:00
scripts Typescript Port - Init 2026-03-21 10:52:03 +01:00
src no more orange :) 2026-03-21 12:02:59 +01:00
tests boundary cache 2026-03-21 14:14:46 +01:00
.gitignore boundary cache 2026-03-21 14:14:46 +01:00
.npmignore Typescript Port - Init 2026-03-21 10:52:03 +01:00
.npmrc Typescript Port - Init 2026-03-21 10:52:03 +01:00
LICENSE Initial commit 2026-03-21 10:48:22 +01:00
package-lock.json Typescript Port - Init 2026-03-21 10:52:03 +01:00
package.json Typescript Port - Init 2026-03-21 10:52:03 +01:00
README.md no more orange :) 2026-03-21 12:02:59 +01:00
tsconfig.json Typescript Port - Init 2026-03-21 10:52:03 +01:00
vitest.config.ts Typescript Port - Init 2026-03-21 10:52:03 +01:00

@polymech/gadm

Pure TypeScript interface to the GADM v4.1 administrative boundaries database.
Zero Python dependencies — parquet data, tree construction, iterators, and caching all run in Node.js.

Overview

Feature Description
Database 356K rows from GADM 4.1, stored as a 6 MB Parquet file
Admin Levels L0 (country) → L5 (municipality/commune)
Tree API Build hierarchical trees, walk with DFS/BFS/level iterators
Name Search Fuzzy search across all levels with Levenshtein suggestions
GeoJSON Fetch boundaries from GADM CDN with corrected names
Caching File-based JSON cache for trees and API results
VARNAME Alternate names / English translations via VARNAME_1..5 columns

Installation

npm install @polymech/gadm

Internal monorepo — referenced via workspace protocol in package.json.


Quick Start

import { buildTree, walkDFS, findNode, searchRegions, getNames } from '@polymech/gadm';

// Build a tree for Spain
const tree = await buildTree({ admin: 'ESP', cacheDir: './cache/gadm' });
console.log(tree.root.children.length); // 18 (comunidades)

// Find a specific region
const bcn = findNode(tree.root, 'Barcelona');
console.log(bcn?.gid); // ESP.6.1_1

// Walk all nodes
for (const node of walkDFS(tree.root)) {
    console.log('  '.repeat(node.level) + node.name);
}

// Search via wrapper API
const result = await searchRegions({ query: 'France', contentLevel: 2 });
console.log(result.data?.length); // ~101 departments

API Reference

Tree Module

buildTree(opts: BuildTreeOptions): Promise<GADMTree>

Builds a hierarchical tree from the flat parquet data. Results are cached to disk when cacheDir is set.

interface BuildTreeOptions {
    name?: string;      // Region name: "Spain", "Cataluña", "Bayern"
    admin?: string;     // GADM code: "ESP", "DEU.2_1", "FRA.11_1"
    cacheDir?: string;  // Path for JSON cache files (optional)
}

Either name or admin must be set (not both).
Throws if the region is not found in the database.

GADMTree and GADMNode

interface GADMTree {
    root: GADMNode;     // Root node of the tree
    maxLevel: number;   // Deepest admin level reached (05)
    nodeCount: number;  // Total nodes across all levels
}

interface GADMNode {
    name: string;           // Display name: "Barcelona"
    gid: string;            // GADM ID: "ESP.6.1_1"
    level: number;          // Admin level 05
    children: GADMNode[];   // Sub-regions (sorted alphabetically)
}

Iterators

All iterators are generators — use for...of or spread into arrays.

Function Description
walkDFS(node) Depth-first traversal, top-down
walkBFS(node) Breadth-first, level by level
walkLevel(node, level) Only nodes at a specific admin level
leaves(node) Only leaf nodes (deepest, no children)
findNode(root, query) First DFS match by name or GID (case-insensitive)
// Get all provinces (level 2) under Cataluña
const provinces = [...walkLevel(tree.root, 2)];
// → [{ name: 'Barcelona', ... }, { name: 'Girona', ... }, ...]

// Count municipalities
const municipios = [...leaves(tree.root)];
console.log(municipios.length); // 955

// Find by GID
const girona = findNode(tree.root, 'ESP.6.2_1');

Names Module

getNames(opts: NamesOptions): Promise<NamesResult>

Searches the parquet database for admin areas. Returns deduplicated rows with fuzzy match suggestions on miss.

interface NamesOptions {
    name?: string;          // Search by name
    admin?: string;         // Search by GADM code
    contentLevel?: number;  // Target level (05), -1 = auto
    complete?: boolean;     // Return all columns up to contentLevel
}

interface NamesResult {
    rows: GadmRow[];    // Matched records
    level: number;      // Resolved content level
    columns: string[];  // Column names in result
}

On miss, throws with Levenshtein-based suggestions:

The requested "Franec" is not part of GADM.
The closest matches are: France, Franca, Franco, ...

Items Module

getItems(opts: ItemsOptions): Promise<GeoJSONCollection>

Fetches GeoJSON boundaries from the GADM CDN, with name correction from the local parquet database (workaround for camelCase bug in GADM GeoJSON responses).

interface ItemsOptions {
    name?: string | string[];   // Region name(s)
    admin?: string | string[];  // GADM code(s)
    contentLevel?: number;      // Target level, -1 = auto
    includeOuter?: boolean;     // Also include the containing region's external perimeter
}

Supports continent expansion: getItems({ name: ['europe'] }) fetches all European countries.


Wrapper Module (Server API)

Higher-level API designed for HTTP handlers. Includes file-based caching via GADM_CACHE env var (default: ./cache/gadm).

Function Description
searchRegions(opts) Search by name, returns metadata or GeoJSON
getBoundary(gadmId, contentLevel?, cache?) Get GeoJSON boundary for a GADM ID
getRegionNames(opts) List sub-region names with depth control

Note on External Caching: By default, boundaries and search results are cached to the local file system (using GADM_CACHE or ./cache/gadm). You can inject an external cache by providing an object implementing { get(key): Promise<any>, set(key, val): Promise<void> } as the cache property in opts (or as the 3rd argument to getBoundary).

Handling Outer vs Inner Boundaries

When integrating with top-level APIs (e.g., handleGetRegionBoundary), you can fetch either the fully merged outer outline or individual inner sub-regions by utilizing the contentLevel and includeOuter parameters:

  • Outer Boundary: Omit contentLevel (or omit passing it up from query strings like outer=true). The engine automatically dissolves geometries and returns a single FeatureCollection with one merged geometry encompassing the entire region (e.g., getBoundary('DEU') returns the border of Germany).
  • Inner Boundaries: Explicitly provide contentLevel as the target child level (e.g., outer=false implies targetLevel = currentLevel + 1). The engine groups geometries at that level, returning a FeatureCollection with multiple features for each subdivision (e.g., getBoundary('DEU', 1) returns the 16 Bundesländer).
  • Combined (Both): Pass the includeOuter flag alongside a higher contentLevel to fetch both. The single outer boundary is injected as the first feature in the FeatureCollection and is distinctly labeled with properties: { isOuter: true } to easily apply CSS or map layer styling to the perimeter outline while rendering its internal geometry separately.

Database Module (Low-Level)

Function Description
loadDatabase() Load parquet into memory (lazy, singleton)
getColumns() Return column names
resetCache() Clear the in-memory row cache

GadmRow is Record<string, string> — all values normalized to strings.


Types

All types are exported from the package entry point:

import type {
    GADMNode, GADMTree, BuildTreeOptions,     // tree
    NamesOptions, NamesResult, GadmRow,       // names + database
    ItemsOptions, GeoJSONFeature, GeoJSONCollection,  // items
    SearchRegionsOptions, SearchRegionsResult, RegionNamesOptions,  // wrapper
} from '@polymech/gadm';

Data Layout

Parquet File

data/gadm_database.parquet356,508 rows, 6.29 MB

Column Group Columns Description
GID GID_0GID_5 GADM identifiers per level
NAME NAME_0NAME_5 Display names per level
VARNAME VARNAME_1VARNAME_5 Alternate names / translations

129,448 rows have VARNAME_1 values (e.g. Badakhshān, Bavière).

GADM Levels

Level Typical Meaning Example (Spain)
0 Country Spain
1 State / Region Cataluña
2 Province / Department Barcelona
3 District / Comarca Baix Llobregat
4 Municipality Castelldefels
5 Sub-municipality (rare, not all countries)

Note: GADM does not include neighborhood/Stadtteil-level data.
For sub-city resolution (e.g. Johannstadt in Dresden), OSM/Nominatim would be needed.


Caching

Tree Cache (cacheDir)

When cacheDir is passed to buildTree(), the full tree is saved as tree_{md5}.json.
Subsequent calls with the same name/admin return the cached tree instantly (~1ms).

Wrapper Cache (GADM_CACHE)

The wrapper module caches search results, boundaries, and region names in $GADM_CACHE/ (default ./cache/gadm).
Files are keyed by MD5 hash of the query parameters.

In-Memory Cache

loadDatabase() is a singleton — the 356K-row array is loaded once per process.
Call resetCache() to force a reload (useful in tests).

Precalculating Boundaries

To improve runtime performance (especially for large geographies which take time to dissolve), you can precalculate and cache standard admin boundaries using the included CLI script:

cd packages/gadm

# Precalculate the outer boundary for a specific country
npm run boundaries -- --country=DEU

# Precalculate inner boundaries for a specific level
npm run boundaries -- --country=DEU --level=1

# Precalculate the outer boundary for ALL countries worldwide
npm run boundaries -- --country=all

Precalculated boundaries are saved as native .json artifacts inside the configured cache directory (./cache/gadm/boundary_{md5}.json).


Data Refresh

Regenerate data/gadm_database.parquet from a GADM GeoPackage source file.

Prerequisites

Download one of the GeoPackage files into server/cache/gadm/:

https://geodata.ucdavis.edu/gadm/gadm4.1/gadm_410-gpkg.zip  → unzip → gadm_410.gpkg
https://geodata.ucdavis.edu/gadm/gadm4.1/gadm_410-raw.gpkg

Run

cd packages/gadm
npm run refresh

The script (scripts/refresh-database.ts):

  1. Opens the GeoPackage (SQLite) via better-sqlite3
  2. Auto-detects table format (per-level ADM_x tables or single flat table)
  3. Extracts GID, NAME, and VARNAME columns for levels 05
  4. Writes to data/gadm_database.parquet via hyparquet-writer

Dev Dependencies (refresh only)

Package Purpose
better-sqlite3 Read GeoPackage (SQLite) files
hyparquet-writer Write Parquet output

These are devDependencies — not needed at runtime.


Tests

cd packages/gadm
npx vitest run                              # all tests
npx vitest run src/__tests__/tree.test.ts   # tree tests only

Tree Tests

JSON outputs saved to tests/tree/ for inspection:

File Content
test-cataluna.json Full Cataluña tree (1,000 nodes, 955 leaves)
test-germany-summary.json Germany L1 summary (16 Bundesländer, 16,402 nodes)
test-dresden.json Sachsen → Dresden subtree with all children
test-iterators.json DFS/BFS/walkLevel/findNode verification data

Name Tests

src/__tests__/province-names.test.ts — tests getNames() for France departments, exact matches, fuzzy suggestions.


Architecture

packages/gadm/
├── data/
│   ├── gadm_database.parquet     # 356K rows, 6.29 MB
│   └── gadm_continent.json       # Continent → ISO3 mapping
├── scripts/
│   └── refresh-database.ts       # GeoPackage → Parquet converter
├── src/
│   ├── database.ts               # Parquet reader (hyparquet)
│   ├── names.ts                  # Name/code lookup + fuzzy match
│   ├── items.ts                  # GeoJSON boundaries from CDN
│   ├── wrapper.ts                # Server-facing API with cache
│   ├── tree.ts                   # Tree builder + iterators
│   ├── index.ts                  # Barrel exports
│   └── __tests__/
│       ├── tree.test.ts          # Tree building + iterator tests
│       └── province-names.test.ts
├── tests/
│   ├── tree/                     # Test output JSONs
│   └── cache/gadm/               # Tree cache files
└── package.json

Dependencies

Package Type Purpose
hyparquet runtime Read Parquet files (zero native deps)
zod runtime Schema validation
better-sqlite3 dev GeoPackage reader (refresh only)
hyparquet-writer dev Parquet writer (refresh only)
vitest dev Test runner
typescript dev Build