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

geopackage-ts

v0.1.1

Published

Modern TypeScript GeoPackage implementation for Node.js

Readme

geopackage-ts

A modern, backend-only TypeScript implementation of the OGC GeoPackage Encoding Standard. Read, write, and query .gpkg files with a clean synchronous API, zero browser dependencies, and full geometry serialization built from scratch.

Features

  • Full GeoPackage support: features, tiles, and attributes with complete OGC spec compliance
  • Synchronous API: powered by better-sqlite3, no Promises needed
  • Zero geometry dependencies: WKB, WKT, and GeoJSON serialization implemented from scratch (no @ngageoint/simple-features-*)
  • R-tree spatial index: create and query spatial indexes using SQLite's built-in R-tree module
  • Lazy iteration: query results returned as IterableIterator<T> for memory-efficient streaming
  • Dual CJS/ESM: ships both CommonJS and ES module builds with full type declarations
  • Minimal footprint: only 2 runtime dependencies (better-sqlite3, proj4)

Requirements

  • Node.js >= 22
  • A C++ compiler toolchain (required by better-sqlite3 native addon)

Installation

npm install geopackage-ts

Quick Start

Open and query an existing GeoPackage

import { GeoPackageManager } from 'geopackage-ts';

// Open a GeoPackage file
const gp = GeoPackageManager.open('countries.gpkg');

// List tables
console.log('Feature tables:', gp.getFeatureTables());
console.log('Tile tables:', gp.getTileTables());

// Query features as GeoJSON
for (const feature of gp.queryForGeoJSONFeatures('countries')) {
  console.log(feature.properties.name, feature.geometry.type);
}

gp.close();

Create a new GeoPackage

import { GeoPackageManager, GeoPackageDataType, buildGeometryData, writeGeometryData } from 'geopackage-ts';
import type { Point, UserColumn } from 'geopackage-ts';

const gp = GeoPackageManager.create('cities.gpkg');

// Define additional columns
const columns: UserColumn[] = [
  {
    index: 2, name: 'name', dataType: GeoPackageDataType.TEXT,
    notNull: false, defaultValue: null, primaryKey: false,
    autoincrement: false, unique: false,
  },
  {
    index: 3, name: 'population', dataType: GeoPackageDataType.INTEGER,
    notNull: false, defaultValue: null, primaryKey: false,
    autoincrement: false, unique: false,
  },
];

// Create a feature table with EPSG:4326
gp.createFeatureTable('cities', 'geom', 'POINT', 4326, columns);

// Insert a feature
const dao = gp.getFeatureDao('cities');
const point: Point = { type: 'Point', hasZ: false, hasM: false, coordinates: [13.405, 52.52] };
const geomBuffer = writeGeometryData(buildGeometryData(point, 4326));

dao.insert({
  table: dao.getTable(),
  values: { geom: geomBuffer, name: 'Berlin', population: 3645000 },
});

// Create a spatial index for fast bounding box queries
gp.indexFeatureTable('cities');

gp.close();

Spatial queries

const gp = GeoPackageManager.open('cities.gpkg');
const dao = gp.getFeatureDao('cities');

// Query features within a bounding box (uses R-tree if available)
for (const row of dao.queryWithBoundingBox({ minX: 5, maxX: 15, minY: 47, maxY: 55 })) {
  console.log(row.name);
}

// Or get GeoJSON directly
for (const feature of dao.queryForGeoJSONWithBoundingBox({ minX: 5, maxX: 15, minY: 47, maxY: 55 })) {
  console.log(feature.properties, feature.geometry);
}

gp.close();

Working with tiles

const gp = GeoPackageManager.create('map.gpkg');

// Create a tile table
gp.createTileTable('world', 3857, {
  minX: -20037508.34, minY: -20037508.34,
  maxX: 20037508.34, maxY: 20037508.34,
});

// Insert tiles
const dao = gp.getTileDao('world');
const pngData = fs.readFileSync('tile_0_0_0.png');
dao.insert({ zoom_level: 0, tile_column: 0, tile_row: 0, tile_data: pngData });

// Query tiles
const tile = dao.queryForTile(0, 0, 0);
if (tile) {
  fs.writeFileSync('output.png', tile.tile_data);
}

// Get available zoom levels
console.log('Zoom levels:', dao.getZoomLevels());

gp.close();

Working with attributes

const gp = GeoPackageManager.create('data.gpkg');

const cols: UserColumn[] = [
  { index: 1, name: 'key', dataType: GeoPackageDataType.TEXT, notNull: true, defaultValue: null, primaryKey: false, autoincrement: false, unique: false },
  { index: 2, name: 'value', dataType: GeoPackageDataType.TEXT, notNull: false, defaultValue: null, primaryKey: false, autoincrement: false, unique: false },
];

gp.createAttributeTable('config', cols);
const dao = gp.getAttributeDao('config');

dao.insert({ table: dao.getTable(), values: { key: 'version', value: '1.0' } });

for (const row of dao.queryForAll()) {
  console.log(row);
}

gp.close();

Geometry serialization

import { readWKB, writeWKB, readWKT, writeWKT, fromGeoJSON, toGeoJSON, ByteOrder } from 'geopackage-ts';

// WKB round-trip
const point = { type: 'Point' as const, hasZ: false, hasM: false, coordinates: [1.5, 2.5] };
const wkb = writeWKB(point, ByteOrder.LITTLE_ENDIAN);
const parsed = readWKB(wkb); // { type: 'Point', coordinates: [1.5, 2.5], ... }

// WKT round-trip
const geom = readWKT('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))');
const wkt = writeWKT(geom); // 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))'

// GeoJSON conversion
const gjson = toGeoJSON(geom);       // standard GeoJSON geometry object
const back = fromGeoJSON(gjson);      // internal Geometry type

// GeoPackage Binary (GP header + envelope + WKB)
import { buildGeometryData, writeGeometryData, readGeometryData } from 'geopackage-ts';
const gd = buildGeometryData(geom, 4326);
const blob = writeGeometryData(gd);  // ready to store in a feature table
const decoded = readGeometryData(blob);
console.log(decoded.srsId, decoded.envelope, decoded.geometry);

Coordinate transforms

import { createTransformFromEPSG, transformBoundingBox } from 'geopackage-ts';

const toWebMercator = createTransformFromEPSG(4326, 3857);
const [x, y] = toWebMercator(13.405, 52.52);

const bbox = transformBoundingBox(
  { minX: -180, minY: -85, maxX: 180, maxY: 85 },
  toWebMercator,
);

Validation

import { GeoPackageManager } from 'geopackage-ts';

const result = GeoPackageManager.validate('data.gpkg');
if (!result.valid) {
  console.error('Validation errors:', result.errors);
}

API Overview

Core Classes

| Class | Description | |---|---| | GeoPackageManager | Factory for opening, creating, and validating GeoPackage files | | GeoPackage | Main entry point: table listing, DAO access, feature/tile/attribute creation | | GeoPackageConnection | Low-level better-sqlite3 wrapper (query, exec, pragmas, custom functions) |

Data Access Objects

| DAO | Description | |---|---| | FeatureDao | Query, insert, update, delete features; GeoJSON iteration; bounding box queries | | TileDao | Query, insert, delete tiles by zoom/column/row; zoom level management | | AttributeDao | CRUD for non-spatial attribute tables | | UserDao | Base DAO with generic query/insert/update/delete operations |

Geometry

| Function | Description | |---|---| | readWKB / writeWKB | Parse and write OGC Well-Known Binary (all types, Z/M, both byte orders) | | readWKT / writeWKT | Parse and write Well-Known Text | | fromGeoJSON / toGeoJSON | Convert between GeoJSON and internal geometry types | | readGeometryData / writeGeometryData | Decode/encode GeoPackage Binary format (GP header + envelope + WKB) | | computeEnvelope | Compute bounding box for any geometry | | createPoint / createLineString / createPolygon | Factory helpers |

Extensions

| Function | Description | |---|---| | createRTreeIndex | Create an R-tree spatial index with auto-sync triggers | | isRTreeIndexed | Check if a feature table has an R-tree index | | ensureDataColumnsTables | Set up the schema extension tables | | ensureMetadataTables | Set up the metadata extension tables |

Projection

| Function | Description | |---|---| | createTransform | Create a transform function from SRS definitions | | createTransformFromEPSG | Create a transform function from EPSG codes | | transformBoundingBox | Reproject a bounding box | | registerProjection | Register a custom projection with proj4 |

Types

All GeoPackage spec table structures are available as TypeScript interfaces:

BoundingBox, SpatialReferenceSystem, Contents, GeometryColumns, TileMatrixSet, TileMatrix, Extension, ValidationResult, ColumnDefinition, TableDefinition

Geometry types use a discriminated union:

Geometry = Point | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygon | GeometryCollection

Architecture

src/
├── index.ts                     # Public API exports
├── geopackage.ts                # Main GeoPackage class
├── geopackage-manager.ts        # Open / create / validate
├── types.ts                     # Shared types, enums, constants, errors
├── db/
│   ├── connection.ts            # better-sqlite3 wrapper
│   └── table-creator.ts         # DDL for all GeoPackage system tables
├── core/
│   ├── srs.ts                   # gpkg_spatial_ref_sys operations
│   ├── contents.ts              # gpkg_contents operations
│   └── extensions.ts            # gpkg_extensions operations
├── geom/
│   ├── geometry.ts              # Geometry types + envelope utilities
│   ├── geometry-data.ts         # GeoPackage Binary header encode/decode
│   ├── wkb/                     # WKB reader/writer
│   ├── wkt/                     # WKT reader/writer
│   └── geojson/                 # GeoJSON reader/writer
├── features/                    # Feature table, DAO, geometry columns
├── tiles/                       # Tile table, DAO, matrix, utilities
├── attributes/                  # Attribute table and DAO
├── user/                        # Base table/row/column/DAO abstractions
├── extension/
│   ├── rtree-index.ts           # R-tree spatial index
│   ├── schema.ts                # Data columns extension
│   └── metadata.ts              # Metadata extension
├── projection/                  # proj4 wrapper for coordinate transforms
└── io/                          # File copy, export, validation

Documentation

API documentation is generated with TypeDoc. All public APIs have comprehensive TSDoc comments with @param, @returns, @throws, and @example tags.

npm run docs

The generated documentation will be in the docs/ directory.

Scripts

npm run build          # Build dual CJS/ESM output to dist/
npm test               # Run all tests
npm run test:watch     # Run tests in watch mode
npm run test:coverage  # Run tests with coverage
npm run lint           # Lint with Biome
npm run format         # Format with Biome
npm run docs           # Generate API docs with TypeDoc

Migration Guide from @ngageoint/geopackage

This section covers the key differences when migrating server-side code from @ngageoint/geopackage (v4.x) to geopackage-ts.

1. Installation

- npm install @ngageoint/geopackage
+ npm install geopackage-ts

No WASM setup required. No setSqljsWasmLocateFile() or setCanvasKitWasmLocateFile() calls.

2. Opening a GeoPackage

- import { GeoPackageManager } from '@ngageoint/geopackage';
- const gp = await GeoPackageManager.open(filePath);
+ import { GeoPackageManager } from 'geopackage-ts';
+ const gp = GeoPackageManager.open(filePath);

The entire API is synchronous. Remove all await keywords and .then() chains when calling GeoPackage methods. This is a direct consequence of using better-sqlite3 instead of sql.js.

3. Opening from a Buffer

- const gp = await GeoPackageManager.open(buffer);
+ const gp = GeoPackageManager.open(buffer);

Both string (file path) and Buffer are accepted by the same open() method.

4. Creating a GeoPackage

- const gp = await GeoPackageManager.create(filePath);
+ const gp = GeoPackageManager.create(filePath);

The created GeoPackage is automatically initialized with the required system tables and 4 default SRS entries (undefined cartesian, undefined geographic, EPSG:4326, EPSG:3857).

5. Querying features

- // Old: callback or array-based
- const rows = gp.queryForGeoJSONFeaturesInTable('rivers');
- rows.forEach(feature => { ... });

+ // New: lazy iterator
+ for (const feature of gp.queryForGeoJSONFeatures('rivers')) {
+   // feature is a standard GeoJSON Feature
+   console.log(feature.properties, feature.geometry);
+ }

Result sets are returned as IterableIterator<T>, not arrays. Use for...of to iterate lazily, or [...iterator] to collect into an array. No .close() call needed on result sets: better-sqlite3 handles cleanup automatically.

6. Feature DAO

- const dao = gp.getFeatureDao('my_table');
- const resultSet = dao.queryForAll();
- while (resultSet.moveToNext()) {
-   const row = resultSet.getRow();
-   const geometry = row.getGeometry().getGeometry();
- }
- resultSet.close();

+ const dao = gp.getFeatureDao('my_table');
+ for (const feature of dao.queryForGeoJSON()) {
+   // Standard GeoJSON Feature:  no manual geometry decoding needed
+   console.log(feature.geometry.type, feature.properties);
+ }

7. Inserting features

- import { GeoPackageGeometryData } from '@ngageoint/geopackage';
- import { Point } from '@ngageoint/simple-features-js';
- const geomData = new GeoPackageGeometryData();
- geomData.setGeometry(new Point(13.405, 52.52));
- geomData.setSrsId(4326);
- const featureRow = dao.newRow();
- featureRow.setGeometry(geomData);
- featureRow.setValue('name', 'Berlin');
- dao.create(featureRow);

+ import { buildGeometryData, writeGeometryData } from 'geopackage-ts';
+ import type { Point } from 'geopackage-ts';
+ const point: Point = { type: 'Point', hasZ: false, hasM: false, coordinates: [13.405, 52.52] };
+ const geomBuffer = writeGeometryData(buildGeometryData(point, 4326));
+ dao.insert({ table: dao.getTable(), values: { geom: geomBuffer, name: 'Berlin' } });

8. Geometry types

- // Old: class-based NGA geometry hierarchy
- import { Point, LineString, Polygon } from '@ngageoint/simple-features-js';
- const point = new Point(1, 2);
- point.hasZ = true;
- point.z = 100;

+ // New: plain objects with discriminated union
+ import type { Point } from 'geopackage-ts';
+ const point: Point = { type: 'Point', hasZ: true, hasM: false, coordinates: [1, 2, 100] };

No @ngageoint/simple-features-js, @ngageoint/simple-features-wkb-js, @ngageoint/simple-features-geojson-js, or @ngageoint/simple-features-proj-js packages needed. All geometry serialization is built in.

9. Spatial queries with R-tree index

- import { RTreeIndexExtension } from '@ngageoint/geopackage';
- const rtree = new RTreeIndexExtension(gp);
- rtree.createWithFeatureTable(featureTable);
- const featureIndexManager = new FeatureIndexManager(gp, 'my_table');
- featureIndexManager.setIndexLocation(FeatureIndexType.RTREE);
- const results = featureIndexManager.queryWithBoundingBox(bbox, projection);

+ // Create the index
+ gp.indexFeatureTable('my_table');
+
+ // Query:  automatically uses R-tree if available
+ const dao = gp.getFeatureDao('my_table');
+ for (const row of dao.queryWithBoundingBox({ minX: 0, maxX: 10, minY: 0, maxY: 10 })) {
+   console.log(row);
+ }

10. Tile access

- const tileDao = gp.getTileDao('my_tiles');
- const resultSet = tileDao.queryForTile(col, row, zoom);
- if (resultSet.getCount() > 0) {
-   resultSet.moveToNext();
-   const tileRow = resultSet.getRow();
-   const tileData = tileRow.getTileData();
- }
- resultSet.close();

+ const tileDao = gp.getTileDao('my_tiles');
+ const tile = tileDao.queryForTile(col, row, zoom);
+ if (tile) {
+   const tileData = tile.tile_data; // Buffer
+ }

11. Projections

- import { ProjectionFactory, Projections } from '@ngageoint/projections-js';
- const proj = ProjectionFactory.getProjection(Projections.EPSG_4326);

+ import { createTransformFromEPSG } from 'geopackage-ts';
+ const transform = createTransformFromEPSG(4326, 3857);
+ const [x, y] = transform(lon, lat);

12. What's been removed

The following features from @ngageoint/geopackage are intentionally not included:

| Removed | Reason | |---|---| | Canvas / CanvasKit rendering | Backend-only: no image generation | | FeatureTiles / drawTile() | Tile rendering is out of scope | | sql.js / WASM SQLite | Replaced by native better-sqlite3 | | Browser support | Node.js only | | setCanvasKitWasmLocateFile() | No WASM dependencies | | setSqljsWasmLocateFile() | No WASM dependencies | | Web Worker support | Not needed server-side | | Leaflet integration | No map framework dependencies | | Feature simplification | No simplify-js dependency | | NGA geometry class hierarchy | Replaced with discriminated unions | | 6-level DAO inheritance | Simplified to flat DAO classes | | Related Tables extension | Not yet implemented | | NGA Contents ID extension | Not yet implemented | | NGA Feature Tile Link | Not yet implemented |

Quick reference

| @ngageoint/geopackage | geopackage-ts | |---|---| | await GeoPackageManager.open(path) | GeoPackageManager.open(path) | | await GeoPackageManager.create(path) | GeoPackageManager.create(path) | | new Point(x, y) | { type: 'Point', hasZ: false, hasM: false, coordinates: [x, y] } | | createPoint(x, y) | createPoint(x, y) | | resultSet.moveToNext() / .close() | for (const row of dao.queryForAll()) | | featureRow.getGeometry() | readGeometryData(row.geom) | | geoPackageGeometryData.setGeometry(pt) | writeGeometryData(buildGeometryData(pt, srsId)) | | dao.create(row) | dao.insert({ table, values }) | | new RTreeIndexExtension(gp) | gp.indexFeatureTable(tableName) | | featureIndexManager.queryWithBoundingBox() | dao.queryWithBoundingBox(bbox) |

License

MIT