Browser-based geocoding SDK using DuckDB-WASM
npm install @tabaqat/geocoding-sdk




v0.2.1 - A browser-based geocoding SDK for Saudi Arabia using DuckDB-WASM. Zero backend dependencies - runs entirely in the browser with automatic fallback to default data source.
Check out the interactive documentation and demos:
- Try the interactive playground to see the SDK in action.
- React Example App - Comprehensive demo with all SDK features
- Playground with forward/reverse geocoding, postcode search
- Full API documentation with interactive examples
- MapLibre GL integration example
- Arabic/English RTL/LTR support
``mermaid
flowchart TB
subgraph DataSources["Data Sources"]
OA[("OpenAddresses
5.3M addresses")]
OM[("Overture Maps
Street names")]
GADM[("GADM
Boundaries")]
end
subgraph BuildPipeline["Build Pipeline (Python)"]
direction TB
B1["Load & Clean CSV"]
B2["Enrich with Admin Boundaries
(Spatial Join)"]
B3["Enrich Streets
(Overture Maps)"]
B4["Generate H3 Tiles
(Resolution 5)"]
B5["Create Indexes"]
end
subgraph CDN["CDN Storage (source.coop)"]
TI[("tile_index.parquet
717 tiles metadata")]
PI[("postcode_index.parquet
6,499 postcodes")]
TILES[("tiles/*.parquet
~220KB avg each")]
BOUNDS[("Boundary files
countries/regions/districts")]
end
subgraph Browser["Browser Runtime"]
SDK["GeoSDK"]
DUCKDB["DuckDB-WASM"]
CACHE["Tile Cache + Search Cache"]
subgraph Queries["Query Types"]
FWD["Forward Geocode
(text → coords)"]
REV["Reverse Geocode
(coords → address)"]
POST["Postcode Search"]
NUM["House Number Search"]
end
end
OA --> B1
OM --> B3
GADM --> B2
B1 --> B2 --> B3 --> B4 --> B5
B5 --> TI & PI & TILES & BOUNDS
SDK --> DUCKDB
DUCKDB <-->|"HTTP Range Requests"| CDN
DUCKDB --> CACHE
SDK --> Queries
TI -.->|"Region/Bbox Filter"| FWD
PI -.->|"Postcode → Tiles"| POST
TILES -.->|"H3 Tile Lookup"| REV
TI -.->|"Region Filter"| NUM
`
1. Initialization (~140KB): Loads tile index, postcode index, and boundary files
2. Reverse Geocoding: Computes H3 cell from coordinates → loads only that tile (~220KB)
3. Forward Geocoding: Uses region/bbox filters → queries only relevant tiles
4. Postcode Search: Looks up postcode in index → loads mapped tiles (avg 1.3 tiles)
5. All queries: DuckDB-WASM executes SQL on Parquet via HTTP range requests
- Forward Geocoding: Convert addresses to coordinates with FTS/BM25 or Jaccard similarity matching
- Smart Geocoding: Auto-detects postcodes and regions in query for optimized routing
- Cached Geocoding: LRU cache (100 entries, 5min TTL) for faster repeated searches
- Reverse Geocoding: Find nearest addresses from coordinates with H3 tile-based spatial indexing
- Postcode Search: Ultra-fast postcode lookups using indexed tile mapping (~1.3 tiles per postcode)
- House Number Search: Search by building number with region/bbox filtering
- Country Detection: Identify country from coordinates using spatial containment
- Admin Hierarchy: Get district, governorate, and region info for Saudi Arabia coordinates
- Autocomplete Suggestions: Get district, postcode, and region suggestions for typeahead
- H3 Tile Loading: Ultra-fast on-demand loading (~220KB average per tile)
- Region Filtering: Filter searches by single or multiple region names
- Bbox Filtering: Optimize forward geocoding by limiting search to visible map area
- Debug Logging: Configurable logging with multiple levels
- Bilingual: Full support for Arabic and English
| Metric | Value |
| ------------------------ | --------------------------- |
| Reverse geocode (cold) | < 4 seconds |
| Reverse geocode (cached) | < 100ms |
| Forward geocode (bbox) | 2-5 seconds |
| Forward geocode (cached) | < 500ms |
| Postcode search | < 500ms (avg 1.3 tiles) |
| Average tile size | 220 KB |
| Largest tile | 6 MB |
| Total tiles | 717 |
| Total postcodes | 6,499 |
| Initial load | ~140 KB |
`bashnpm
npm install @tabaqat/geocoding-sdk
Quick Start
$3
`typescript
import { GeoSDK } from '@tabaqat/geocoding-sdk';// Initialize SDK with options
const sdk = new GeoSDK({
debug: true, // Enable debug logging
logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'none'
});
await sdk.initialize();
// Forward geocoding (address → coordinates)
const results = await sdk.geocode('حي الروضة الرياض');
console.log(results[0]);
// { addr_id: 123, latitude: 24.7, longitude: 46.6, full_address_ar: '...', similarity: 0.85 }
// Reverse geocoding (coordinates → address) - ultra fast!
const nearby = await sdk.reverseGeocode(24.7136, 46.6753);
console.log(nearby[0]);
// { addr_id: 456, distance_m: 50, full_address_ar: '...' }
// Country detection
const country = await sdk.detectCountry(24.7136, 46.6753);
console.log(country);
// { iso_a2: 'SA', name_ar: 'المملكة العربية السعودية', name_en: 'Saudi Arabia' }
`$3
`typescript
// Smart geocode auto-detects query patterns for optimized routing
// Detects postcode → routes to searchByPostcode()
const byPostcode = await sdk.smartGeocode('12345');// Detects region in query → adds region filter
const withRegion = await sdk.smartGeocode('مكة الرياض');
// Regular query → uses standard geocode()
const regular = await sdk.smartGeocode('حي النخيل');
`$3
`typescript
// Use LRU cache for faster repeated searches (100 entries, 5min TTL)
const results = await sdk.geocodeCached('الرياض', { limit: 10 });// Subsequent identical searches return cached results instantly
const cached = await sdk.geocodeCached('الرياض', { limit: 10 }); // < 1ms
// Clear cache manually when needed
sdk.clearCache();
`$3
`typescript
// Search by postcode - only loads 1-3 tiles (avg 1.3)
const results = await sdk.searchByPostcode('13847');
console.log(results);
// All addresses with this postcode// Combine with house number for exact match
const exact = await sdk.searchByPostcode('13847', { number: '2808' });
console.log(exact[0]);
// { addr_id: 123, number: '2808', postcode: '13847', ... }
`$3
`typescript
// Search by house number within a specific region
const results = await sdk.searchByNumber('4037', {
region: 'منطقة الرياض', // Filter by region
limit: 10,
});// Or search within visible map area
const results = await sdk.searchByNumber('4037', {
bbox: [24.5, 46.5, 24.9, 47.0],
});
`$3
`typescript
// Get district/postcode/region suggestions for typeahead
const suggestions = await sdk.getAutocompleteSuggestions('الر', {
limit: 10,
types: 'all', // or ['district', 'postcode', 'region']
});
console.log(suggestions);
// [
// { type: 'district', value: 'الروضة', label_ar: 'الروضة', label_en: 'Al Rawdah' },
// { type: 'region', value: 'الرياض', label_ar: 'منطقة الرياض', label_en: 'Riyadh Region' },
// ...
// ]
`$3
`typescript
// Get all postcodes starting with prefix (for autocomplete)
const postcodes = sdk.getPostcodes('138');
console.log(postcodes);
// [{ postcode: '13844', tiles: [...], addr_count: 500, region_ar: 'منطقة الرياض' }, ...]
`$3
`typescript
// Optimize forward geocoding by limiting search to visible map area
const results = await sdk.geocode('شارع الملك فهد', {
limit: 10,
bbox: [24.5, 46.5, 24.9, 47.0], // [minLat, minLon, maxLat, maxLon]
});// Filter by single region
const results = await sdk.geocode('شارع الملك فهد', {
region: 'منطقة الرياض',
});
// Filter by multiple regions
const results = await sdk.geocode('شارع الملك فهد', {
regions: ['منطقة الرياض', 'المنطقة الشرقية'],
});
`$3
`typescript
// Get full admin hierarchy including governorate
const admin = await sdk.getAdminHierarchy(24.7136, 46.6753);
console.log(admin);
// {
// district: { name_ar: 'العليا', name_en: 'Al Olaya' },
// governorate: { name_ar: 'الرياض', name_en: 'Riyadh' },
// region: { name_ar: 'منطقة الرياض', name_en: 'Riyadh Region' }
// }// Quick check if coordinates are in Saudi Arabia
const inSA = await sdk.isInSaudiArabia(24.7136, 46.6753);
console.log(inSA); // true
`$3
`typescript
// Enable debug at initialization
const sdk = new GeoSDK({
debug: true,
logLevel: 'debug', // 'debug' | 'info' | 'warn' | 'error' | 'none'
});// Or toggle at runtime
sdk.setDebug(true);
sdk.setDebug(true, 'debug'); // With specific log level
sdk.setDebug(false); // Disable
`$3
`tsx
import { useState, useEffect } from 'react';
import { GeoSDK } from '@tabaqat/geocoding-sdk';function useGeoSDK() {
const [sdk, setSDK] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [searchMode, setSearchMode] = useState(null);
useEffect(() => {
const init = async () => {
try {
const geoSDK = new GeoSDK({ debug: true });
await geoSDK.initialize();
setSDK(geoSDK);
setSearchMode(geoSDK.getSearchMode()); // 'fts-bm25' or 'jaccard'
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
};
init();
return () => {
sdk?.close();
};
}, []);
return { sdk, loading, error, searchMode };
}
function AddressSearch() {
const { sdk, loading, error, searchMode } = useGeoSDK();
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = async () => {
if (!sdk || !query) return;
// Use cached search for better performance
const addresses = await sdk.geocodeCached(query, { limit: 5 });
setResults(addresses);
};
if (loading) return
Loading geocoder...;
if (error) return Error: {error.message}; return (
Search mode: {searchMode}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search address..."
/>
{results.map((r) => (
{r.full_address_ar} ({(r.similarity * 100).toFixed(0)}%)
))}
);
}
`$3
`vue
Loading geocoder...
{{ r.full_address_ar }} ({{ (r.similarity * 100).toFixed(0) }}%)
`$3
`tsx
'use client';import { useState, useEffect, useCallback } from 'react';
import dynamic from 'next/dynamic';
// Dynamic import to avoid SSR issues with DuckDB-WASM
const GeocoderComponent = dynamic(() => import('./GeocoderComponent'), {
ssr: false,
loading: () =>
Loading geocoder...,
});export default function MapPage() {
return (
Saudi Arabia Geocoder
);
}// GeocoderComponent.tsx (client-only component)
import { GeoSDK } from '@tabaqat/geocoding-sdk';
export default function GeocoderComponent() {
const [sdk, setSDK] = useState(null);
useEffect(() => {
const init = async () => {
const geoSDK = new GeoSDK({ debug: true });
await geoSDK.initialize();
setSDK(geoSDK);
};
init();
return () => {
sdk?.close();
};
}, []);
// ... rest of component
}
`API Reference
$3
The default
GeoSDK uses H3 tile-based partitioning (V3) for optimal performance.#### Constructor
`typescript
const sdk = new GeoSDK(config?: GeoSDKConfig);interface GeoSDKConfig {
dataUrl?: string; // Custom data URL (default: source.coop V3 CDN)
language?: 'ar' | 'en'; // Preferred language
debug?: boolean; // Enable debug logging (default: false)
logLevel?: LogLevel; // 'debug' | 'info' | 'warn' | 'error' | 'none'
}
`####
initialize(options?): PromiseInitialize the SDK. Must be called before any other methods.
`typescript
await sdk.initialize({
onProgress: (step, status, timeMs, details) => {
console.log(${step}: ${status} (${timeMs}ms));
},
});
`####
geocode(address: string, options?: GeocodeOptions): PromiseForward geocoding - convert an address to coordinates.
`typescript
const results = await sdk.geocode('حي الروضة الرياض', {
limit: 10, // Max results (default: 10)
bbox: [24.5, 46.5, 24.9, 47.0], // Optional: limit to visible map area
region: 'منطقة الرياض', // Optional: filter by single region
regions: ['منطقة الرياض', 'مكة'], // Optional: filter by multiple regions
});
`####
geocodeCached(address: string, options?): PromiseCached forward geocoding with LRU cache (100 entries, 5min TTL).
`typescript
const results = await sdk.geocodeCached('الرياض', { limit: 10 });
`####
smartGeocode(query: string, options?): PromiseSmart geocoding that auto-detects query patterns for optimized routing.
`typescript
// Detects postcode → routes to searchByPostcode()
// Detects region → adds region filter
// Otherwise → uses standard geocode()
const results = await sdk.smartGeocode('12345 الرياض');
`####
getAutocompleteSuggestions(query: string, options?): PromiseGet autocomplete suggestions for districts, postcodes, and regions.
`typescript
const suggestions = await sdk.getAutocompleteSuggestions('الر', {
limit: 10,
types: 'all', // or ['district', 'postcode', 'region']
});
// Returns: [{ type, value, label_ar, label_en, metadata? }, ...]
`####
reverseGeocode(lat: number, lon: number, options?): PromiseReverse geocoding - find addresses near coordinates.
`typescript
const nearby = await sdk.reverseGeocode(24.7136, 46.6753, {
limit: 10, // Max results (default: 10)
radiusMeters: 1000, // Search radius (default: 1000)
detailLevel: 'postcode', // Column projection level (default: 'full')
includeNeighbors: false, // Include neighboring H3 tiles (default: false)
});
`Column Projection Optimization (
detailLevel):-
'minimal': Only coordinates + distance (3 columns, ~3MB)
- 'postcode': + postcode + region (6 columns, ~4MB)
- 'region': + district + city (9 columns, ~6MB)
- 'full': All address fields (16 columns, default)####
detectCountry(lat: number, lon: number): PromiseDetect which country a coordinate is in.
`typescript
const country = await sdk.detectCountry(30.0444, 31.2357);
// { iso_a2: 'EG', name_ar: 'مصر', name_en: 'Egypt', continent: 'Africa' }
`####
isInSaudiArabia(lat: number, lon: number): PromiseQuick check if coordinates are in Saudi Arabia.
`typescript
const inSA = await sdk.isInSaudiArabia(24.7136, 46.6753);
// true
`####
getAdminHierarchy(lat: number, lon: number): PromiseGet administrative hierarchy for Saudi Arabia coordinates.
`typescript
const admin = await sdk.getAdminHierarchy(24.7136, 46.6753);
// {
// district: { name_ar: 'العليا', name_en: 'Al Olaya' },
// governorate: { name_ar: 'الرياض', name_en: 'Riyadh' },
// region: { name_ar: 'منطقة الرياض', name_en: 'Riyadh Region' }
// }
`####
searchByPostcode(postcode: string, options?): PromiseUltra-fast postcode search using indexed tile mapping. Only loads 1-3 tiles per query.
`typescript
const results = await sdk.searchByPostcode('13847', {
limit: 50, // Max results (default: 50)
number: '2808', // Optional: filter by house number
});
`####
searchByNumber(number: string, options?): PromiseSearch by house number with region or bbox filtering.
`typescript
const results = await sdk.searchByNumber('4037', {
region: 'منطقة الرياض', // Filter by region name
bbox: [24.5, 46.5, 24.9, 47.0], // Or filter by bbox
limit: 20, // Max results (default: 20)
});
`####
getPostcodes(prefix?: string): PostcodeInfo[]Get available postcodes for autocomplete. Returns from in-memory index (instant).
`typescript
const all = sdk.getPostcodes();
const filtered = sdk.getPostcodes('138');
// [{ postcode: '13844', tiles: ['...'], addr_count: 500, region_ar: '...' }, ...]
`#### Tile Management
`typescript
// Get all tiles info
const tiles = sdk.getTiles();// Get currently loaded tiles
const loaded = sdk.getLoadedTiles();
// Get tiles for a specific region
const riyadhTiles = sdk.getTilesByRegion('منطقة الرياض');
// Get tiles that intersect a bounding box
const bboxTiles = await sdk.getTilesForBbox(24.5, 46.5, 24.9, 47.0);
`#### Debug & Cache
`typescript
// Enable/disable debug at runtime
sdk.setDebug(true);
sdk.setDebug(true, 'debug');// Get search mode
const mode = sdk.getSearchMode(); // 'fts-bm25' or 'jaccard'
// Check if FTS is available
const hasFTS = sdk.isFTSAvailable();
// Clear search cache
sdk.clearCache();
`####
getStats(): PromiseGet statistics about the tile index.
`typescript
const stats = await sdk.getStats();
// { totalTiles: 717, totalAddresses: 5338646, totalSizeKb: 158234, tilesLoaded: 5 }
`####
close(): PromiseClose the database connection and free resources.
`typescript
await sdk.close();
`Result Types
$3
`typescript
interface GeocodingResult {
addr_id: number;
longitude: number;
latitude: number;
number?: string; // Building number
street?: string; // Street name
postcode?: string;
district_ar?: string;
district_en?: string;
city?: string;
gov_ar?: string; // Governorate (Arabic)
gov_en?: string; // Governorate (English)
region_ar?: string;
region_en?: string;
full_address_ar?: string;
full_address_en?: string;
h3_index?: string; // H3 cell index (high resolution)
similarity?: number; // For forward geocoding (0-1)
distance_m?: number; // For reverse geocoding (meters)
}
`$3
`typescript
interface CountryResult {
iso_a3: string; // 'SAU'
iso_a2: string; // 'SA'
name_en: string; // 'Saudi Arabia'
name_ar: string; // 'المملكة العربية السعودية'
continent: string; // 'Asia'
}
`$3
`typescript
interface AdminHierarchy {
district?: { name_ar: string; name_en: string };
governorate?: { name_ar: string; name_en: string };
region?: { name_ar: string; name_en: string };
}
`$3
`typescript
interface PostcodeInfo {
postcode: string; // '13847'
tiles: string[]; // ['85654c3ffffffff'] - H3 tiles containing this postcode
addr_count: number; // Number of addresses with this postcode
region_ar?: string; // 'منطقة الرياض'
region_en?: string; // 'Riyadh Region'
}
`$3
`typescript
interface TileInfo {
h3_tile: string; // '85654c3ffffffff'
addr_count: number; // Addresses in tile
min_lon: number;
max_lon: number;
min_lat: number;
max_lat: number;
file_size_kb: number;
region_ar?: string; // Primary region (Arabic)
region_en?: string; // Primary region (English)
}
`Data Coverage
- 5.3M+ addresses across all 13 Saudi Arabia regions
- 717 H3 tiles using resolution 5 (~250km² each)
- 36% have street names (enriched from Overture Maps)
- 100% have postcode, district, governorate, region
- Average tile size: 220 KB
- Initial load: ~140 KB (metadata + boundaries)
Browser Support
- Chrome 80+
- Firefox 78+
- Safari 14+
- Edge 80+
Requires WebAssembly and Web Workers support.
Custom Data URL
To host your own data files:
`typescript
const sdk = new GeoSDK({
dataUrl: 'https://your-cdn.com/geocoding-data/v0.1.0',
});
`Automatic Fallback: If your custom URL fails, the SDK automatically falls back to the default source.coop CDN.
Your CDN should serve:
-
tile_index.parquet - H3 tile metadata (includes region info)
- postcode_index.parquet - Postcode to tiles mapping
- world_countries_simple.parquet
- sa_regions_simple.parquet
- sa_districts_simple.parquet
- tiles/*.parquet (717 files)Development
`bash
Install dependencies
bun installBuild
bun run buildType check
bun run typecheckLint
bun run lintFormat
bun run format
``Contributions are welcome! This project uses:
- Pre-commit hooks (husky + lint-staged)
- Conventional commits (commitlint)
- ESLint + Prettier for code quality
See RELEASE.md for release process.
MIT
- Address data: OpenAddresses
- Street names: Overture Maps
- Boundaries: GADM
- Query engine: DuckDB-WASM
- Spatial indexing: H3