Skip to main content
The RTree Spatial Index extension (gpkg_rtree_index) stores a SQLite R*Tree virtual table alongside a feature table. The index maps each feature’s row ID to the minimum bounding rectangle (MBR) of its geometry, enabling the database engine to eliminate non-matching rows without reading or parsing geometry blobs.
This is an OGC-registered GeoPackage extension defined at http://www.geopackage.org/spec/#extension_rtree.

How It Works

When the extension is created for a feature table, the library:
  1. Creates a virtual table rtree_<table>_<geometry_column> using SQLite’s rtree module.
  2. Populates it with (id, minx, maxx, miny, maxy) rows derived from each geometry.
  3. Installs triggers on the feature table to keep the index synchronized on insert, update, and delete.
Spatial queries join the R*Tree virtual table against the feature table, letting SQLite prune entire subtrees of the index before touching the main table.

Key Classes

RTreeIndexExtension

Low-level extension class. Manages creation, deletion, and trigger wiring for the R*Tree virtual table.
import { RTreeIndexExtension } from '@ngageoint/geopackage';

const rTree = new RTreeIndexExtension(geoPackage);
const featureDao = geoPackage.getFeatureDao('my_features');
const rTreeDao = rTree.getTableDao(featureDao);

// Check whether the index exists
const isIndexed = rTreeDao.has();

// Create the index
rTreeDao.createExtension();

// Drop the index
rTreeDao.delete();
FeatureIndexManager is the preferred high-level API. It abstracts over multiple index backends and picks the best available one automatically.
import { FeatureIndexManager } from '@ngageoint/geopackage';

const indexManager = new FeatureIndexManager(geoPackage, 'my_features');

FeatureIndexType Enum

ValueDescription
RTREESQLite R*Tree virtual table (fastest; preferred)
GEOPACKAGENGA geometry index stored in GeoPackage extension tables
NONENo index; falls back to a full table scan
The default query order is RTREE → GEOPACKAGE. The manager falls back to the next type automatically when a higher-priority index is not present.

Creating an Index

1

Open the GeoPackage

import { GeoPackageAPI, FeatureIndexManager, FeatureIndexType } from '@ngageoint/geopackage';

const geoPackage = await GeoPackageAPI.open('my.gpkg');
2

Instantiate the index manager

const indexManager = new FeatureIndexManager(geoPackage, 'my_features');
3

Set the index location and build the index

indexManager.setIndexLocation(FeatureIndexType.RTREE);

// Index the table (no-op if already indexed, unless force=true)
const count = indexManager.index();
console.log(`Indexed ${count} features`);
To force a full re-index:
const count = indexManager.indexType(FeatureIndexType.RTREE, /* force */ true);

Querying with a Bounding Box

import { BoundingBox } from '@ngageoint/geopackage';
import { Projections } from '@ngageoint/projections-js';

const bbox = new BoundingBox(-180, -90, 180, 90); // minLon, minLat, maxLon, maxLat
const wgs84 = Projections.getWGS84Projection();

const results = indexManager.queryWithBoundingBoxAndProjection(bbox, wgs84);

for (const featureRow of results) {
  console.log(featureRow.getId(), featureRow.getGeometry());
}

results.close();
Always call results.close() when you are done iterating to release the underlying SQLite statement.

Checking Index Status

// Is any index available?
const isIndexed = indexManager.isIndexed();

// Which index type is being used?
const location = indexManager.getIndexedType();
console.log(FeatureIndexType[location]); // "RTREE"

// Count features in the index
const total = indexManager.count();

Controlling Query Order

You can control which index types are tried and in what order:
import { FeatureIndexType } from '@ngageoint/geopackage';

// Always prefer RTREE
indexManager.prioritizeQueryLocation([FeatureIndexType.RTREE]);

// Override entirely
indexManager.setIndexLocationOrder([FeatureIndexType.RTREE, FeatureIndexType.GEOPACKAGE]);

Deleting an Index

// Delete through the extension directly
const rTree = new RTreeIndexExtension(geoPackage);
const featureDao = geoPackage.getFeatureDao('my_features');
const rTreeDao = rTree.getTableDao(featureDao);
rTreeDao.delete();

// Or via ExtensionManager
geoPackage.getExtensionManager().deleteRTreeSpatialIndex('my_features');

Build docs developers (and LLMs) love