@mrazakassar/audience-identifier-core
v1.4.6
Published
Core audience identification engine with WORLDWIDE reverse geocoding. Auto-detects postal codes globally, generates geohash IDs, and matches audience segments by location, time, and behavior. Works with React Native and Strapi.
Maintainers
Readme
Audience Identifier Core
A powerful NPM package for identifying audience segments based on geographic coordinates, timestamps, and behavioral patterns. Now with WORLDWIDE reverse geocoding and Geohash ID generation. Works seamlessly with React Native, Strapi, and any Node.js environment.
✨ What's New in v1.2.0
🌍 WORLDWIDE Reverse Geocoding - Works globally for ANY latitude/longitude on Earth
🆔 Auto Geohash Generation - Unique geo IDs for location-based indexing & proximity search
📍 Auto ZIP/Postal Code Detection - No API key required (uses free OpenStreetMap Nominatim)
⚡ Zero Configuration - Just coordinates → everything else happens automatically
Features
✅ 13 Pre-defined Audience Segments - Affluent Passengers, Commuters, Airport Travelers, and more ✅ 6 Daypart Classifications - Early Morning, Midday, Evening Peak, Prime Time, Late Night, Overnight ✅ 15 Geographic Zones - Mapped across major cities (extensible globally) ✅ WORLDWIDE Reverse Geocoding - No API key required (uses free OpenStreetMap Nominatim); rate limits apply ✅ Auto Geohash IDs - Unique identifiers for any location (precision 1-12) ✅ Auto ZIP Detection - Automatically detects postal codes from coordinates ✅ Minimal open-source dependencies - Geo uses @turf/bbox, @turf/boolean-point-in-polygon, @turf/union for NYC targeting ✅ Cross-Platform - Works in React Native (via backend), Strapi, Node.js ✅ High Performance - Built-in caching for ZIP-to-zone lookups and reverse geocode (TTL 24h) ✅ Batch Processing - Aggregate statistics from multiple locations
Production recommendations
- Reverse geocoding uses Nominatim (OpenStreetMap), which is an external API. Run it server-side only in production (Node/Express/Koa/Strapi). Do not call Nominatim directly from React Native clients—have them call your backend instead.
- Rate limiting: Nominatim has usage limits; the package caches responses (24h TTL) to reduce repeated calls for the same coordinates. For high traffic, consider self-hosting Nominatim or using a paid geocoding provider.
Installation
Using npm
npm install @ivee/audience-identifier-coreUsing yarn
yarn add @ivee/audience-identifier-coreQuick Start
NEW! Simplest API (v1.2.0+) - AutoDetect Everything
No need to manually get ZIP codes anymore! Just pass coordinates and time:
import { identifyAudience } from '@ivee/audience-identifier-core';
// Inside a component or async function
const getAudience = async () => {
// That's it! ZIP code is auto-detected worldwide
const result = await identifyAudience(
40.7128, // Latitude (anywhere on Earth)
-74.0060, // Longitude (anywhere on Earth)
new Date() // Timestamp
);
console.log(result.audiences); // Matched audience segments
console.log(result.geohashID); // Unique geo ID: "dr5regw"
console.log(result.coordinates); // { lat, lng, zipCode: "10000" }
};1. React Native Example
Production: Call your backend API that uses this package; do not call identifyAudience (and thus Nominatim) directly from the client. That avoids rate limits and keeps API usage server-side.
// In React Native: call your backend
const getAudience = async () => {
const res = await fetch('https://your-api.com/audience/identify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lat: 40.7580, lng: -73.9855, timestamp: new Date().toISOString() }),
});
const result = await res.json();
console.log(result.audiences); // [{ id: 'AUD-06', name: 'Financial / Work / Business', ... }]
};2. Strapi Example
// In your Strapi controller or service
const AudienceService = ({ strapi }) => ({
async identifyUserAudience(lat, lng, timestamp) {
const { identifyAudience } = require('@ivee/audience-identifier-core');
// ZIP code is auto-detected worldwide!
const result = await identifyAudience(lat, lng, timestamp);
return result;
}
});
module.exports = AudienceService;3. Simple Node.js Example
const { identifyAudience, batchIdentifyAudiences } =
require('@ivee/audience-identifier-core');
// Single identification with auto ZIP detection
(async () => {
const result = await identifyAudience(
40.7128,
-74.0060,
'2026-02-05T18:45:00Z'
);
console.log('Postal Code:', result.coordinates.zipCode); // Auto-detected!
console.log('Geohash ID:', result.geohashID); // Geo ID: "dr5regw"
console.log('Matched Audiences:', result.audiences);
console.log('Daypart:', result.daypart.name);
})();4. Worldwide Location Example
const { identifyAudience } = require('@ivee/audience-identifier-core');
(async () => {
// Works ANYWHERE on Earth
const places = [
{ name: 'New York', lat: 40.7128, lng: -74.0060 },
{ name: 'London', lat: 51.5074, lng: -0.1278 },
{ name: 'Tokyo', lat: 35.6762, lng: 139.6503 },
{ name: 'Sydney', lat: -33.8688, lng: 151.2093 }
];
for (const place of places) {
const result = await identifyAudience(place.lat, place.lng, new Date());
console.log(`${place.name}: ${result.coordinates.zipCode} (${result.geohashID})`);
// Output:
// New York: 10000 (dr5regw)
// London: WC2N 5DX (gcpvj0d)
// Tokyo: 168-0063 (xn76cy8)
// Sydney: 2000 (r3gx2fb)
}
})();API Reference
identifyAudience(lat, lng, timestamp, options?) ⭐ RECOMMENDED
NEW in v1.2.0 - Auto-detects ZIP/postal codes worldwide!
Identifies audience segments for a single location and time with automatic geocoding.
Parameters:
lat(number): Latitude (-90 to 90) requiredlng(number): Longitude (-180 to 180) requiredtimestamp(Date|string|number): When the action occurred requiredoptions(Object, optional):rideMode(string): 'work', 'watch', 'shop'startZone(Object): Start zone for commuter logicendZone(Object): End zone for commuter logic
Returns: Promise
{
success: true,
coordinates: { lat, lng, zipCode: "10000" }, // ← Auto-detected!
geohashID: "dr5regw", // ← Auto-generated!
timestamp: "2026-02-05T18:45:00Z",
geoZones: [
{ id: "city-financial-districts", name: "City Financial Districts", ... }
],
daypart: {
id: "evening-peak",
name: "Evening Peak",
startTime: "16:00",
endTime: "19:00"
},
audiences: [
{ id: "AUD-06", name: "Financial / Work / Business", tier: "Intent", ... }
],
metadata: {
isWeekday: true,
queryTimeMs: 12,
geohashPrecision: 7,
version: "1.2.0"
}
}identifyAudienceWithZip(lat, lng, zipCode, timestamp, options?)
Legacy API - If you already have ZIP codes from another geocoding service.
Same as above but takes explicit zipCode parameter.
batchIdentifyAudiences(records)
Process multiple locations efficiently.
Parameters:
records(Array): Array of record objects, each with:{ lat: 40.7128, lng: -74.0060, timestamp: '2026-02-05T18:45:00Z', options: { rideMode: 'work' } }
Returns: Promise - Aggregated statistics
🌍 reverseGeocode(lat, lng, options?) — single reverse geocoding (recommended)
Single flow used by identifyAudience() and for direct use:
- Try OpenStreetMap (Nominatim) first for every call (US or non-US).
- On failure (network/timeout/error), try US cities fallback (sync, no network).
- If not in US fallback, return default postal/zip 10034 (
source: 'default').
No API key required (Nominatim). Responses are cached (TTL 24h). Use server-side only in production.
Parameters:
lat(number): Latitudelng(number): Longitudeoptions(object, optional):{ timeout }— Nominatim timeout in ms (default: 5000)
Returns: Promise
{
city: "New York",
postalCode: "10000",
zipCode: "10000",
country: "United States",
confidence: "high",
source: "nominatim-worldwide" // or "us-fallback" or "default"
}Usage:
const { reverseGeocode } = require('@mrazakassar/audience-identifier-core');
const geo = await reverseGeocode(51.5074, -0.1278); // London → OSM
const us = await reverseGeocode(40.71, -74.01); // NYC → OSM (or US fallback if OSM fails)
// On full failure: { city: "Unknown", postalCode: "10034", zipCode: "10034", source: "default" }reverseGeocodeWorldwide(lat, lng, timeout?) — OSM only (no fallback)
Direct Nominatim (OpenStreetMap) call. No fallback; throws on failure. Use when you only want OSM and will handle errors yourself.
reverseGeocodeSync(lat, lng) — US cities only (sync, no network)
Synchronous US-city lookup only. Returns nearest US city or { city: "Unknown", zipCode: "10004" }. No API calls.
🆔 NEW: Geohash - Unique Geo IDs
NEW in v1.2.0 - Auto-generate unique location IDs
Generates deterministic geohash IDs (like "dr5regw") for spatial indexing and proximity search.
const {
getGeohash, // (lat, lng, precision) => "dr5regw"
getGeohashInfo, // Complete info with all details
decodeGeohash, // Reverse-convert hash to coordinates
getGeohashNeighbors // Find 8 surrounding areas
} = require('@ivee/audience-identifier-core');
// Auto-generated in identifyAudience() response as "geohashID"
const result = await identifyAudience(40.7128, -74.0060, new Date());
console.log(result.geohashID); // "dr5regw" ← Automatically generated!
// Direct usage:
const hash = getGeohash(40.7128, -74.0060, 7);
console.log(hash); // "dr5regw" (~150m precision)
const info = getGeohashInfo(40.7128, -74.0060, 7);
console.log(info.neighbors); // {north, south, east, west, ...}Precision Levels:
- Precision 1: ~5000 km (country level)
- Precision 6: ~1.2 km (city level) ← Default
- Precision 7: ~152m (neighborhood level)
- Precision 8: ~38m (street level)
- Precision 12: ~60cm (building level)
Utility Functions
const {
classifyDaypart, // (timestamp) => {id, name, startTime, endTime}
isWeekday, // (timestamp) => boolean
detectGeoZonesByZip, // (lat, lng, zipCode, options) => [zones]
matchAudiences, // (geoZones, daypart, options) => [audiences]
reverseGeocode, // (lat, lng, options?) => Promise — OSM → US fallback → default 10034
reverseGeocodeSync, // (lat, lng) => US-only sync (no network)
reverseGeocodeWorldwide // (lat, lng, timeout?) => Promise OSM-only
} = require('@ivee/audience-identifier-core');Audience Segments (13 Total)
| ID | Name | Tier | Key Attributes | |----|------|------|---| | AUD-01 | Affluent Passengers | Demo/Psychographic | Luxury zones, daytime hours | | AUD-02 | Airport Travelers | Intent | Airport zones, all dayparts | | AUD-03 | Business Travelers | Intent | Hotels, conventions, business hours | | AUD-04 | Commuters | Intent | Home→Work/Workdays, peak hours | | AUD-05 | Experience Seekers | Interests | Entertainment venues, evenings | | AUD-06 | Financial/Work/Business | Intent | Business zones, workdays only | | AUD-07 | General DMA | Fallback | All zones, all times (catch-all) | | AUD-08 | Hungry Passengers | Purchase Intent | Restaurants, dining times | | AUD-09 | Entertainment Lovers | Interests | Cinema, nightlife, evenings | | AUD-10 | Night Out | Intent | Nightlife, late hours | | AUD-11 | Out of Towners | Intent | Airports, hotels, tourism | | AUD-12 | Retail Shoppers | Purchase Intent | Shopping districts, retail hours | | AUD-13 | Sports Enthusiasts | Interests | Sports venues, event hours |
Dayparts (6 Total)
| Name | Start | End | Use Case | |------|-------|-----|----------| | Early Morning | 05:00 | 09:00 | Commuting, early flights | | Midday | 09:00 | 16:00 | Workday, shopping, lunch | | Evening Peak | 16:00 | 19:00 | Post-work commute | | Prime Time | 19:00 | 23:00 | Entertainment, dining | | Late Night | 23:00 | 02:00 | Nightlife, late dining | | Overnight | 02:00 | 05:00 | Red-eyes, shift work |
Geographic Zones (15 Total)
Mapped across: NYC, Chicago, LA, Miami, Charlotte
- Airport Zones
- Hotel Zones
- City Financial Districts
- Office Hubs & Corporate Campuses
- Major Retail Corridors & Shopping Districts
- Restaurant & Food Districts
- Nightlife Corridors
- Cinema & Theater Zones
- Stadium & Sports Venue Zones
- Cultural & Museum Districts
- Tourism & Visitor Zones
- Affluent Residential Neighborhoods
- General Residential Neighborhoods
- And more...
ZIP Code / Postal Code Handling
✨ NEW: Automatic Detection (v1.2.0+)
You NO LONGER need external geocoding services!
The main identifyAudience() function now automatically detects postal codes worldwide:
const result = await identifyAudience(lat, lng, timestamp);
// ZIP/postal code is auto-detected!
// Works for ANY location on EarthUses FREE Nominatim (OpenStreetMap) API - no API key required!
Legacy: Manual ZIP Code (Optional)
If you already have ZIP codes from another source, you can use the legacy function:
const { identifyAudienceWithZip } = require('@ivee/audience-identifier-core');
const result = await identifyAudienceWithZip(
lat, lng, zipCode, timestamp, options
);Getting ZIP Codes (if needed for legacy code):
For React Native:
For Node.js/Strapi:
Integrating with Strapi
1. Create a Custom Service
// api/audience/services/audience.js
module.exports = {
async identifyAudience(lat, lng, timestamp) {
const { identifyAudience } =
require('@mrazakassar/audience-identifier-core');
// ZIP code is auto-detected worldwide!
return await identifyAudience(lat, lng, timestamp);
},
async batchIdentify(records) {
const { batchIdentifyAudiences } =
require('@mrazakassar/audience-identifier-core');
return await batchIdentifyAudiences(records);
}
};2. Create a POST Controller
// api/audience/controllers/audience.js
module.exports = {
async identify(ctx) {
const { lat, lng, timestamp } = ctx.request.body;
// That's it! No ZIP code needed
const result = await strapi
.service('api::audience.audience')
.identifyAudience(lat, lng, timestamp);
ctx.body = result;
}
};3. Route Configuration
// api/audience/routes/audience.js
module.exports = {
routes: [
{
method: 'POST',
path: '/audience/identify',
handler: 'audience.identify',
config: { policies: [] }
}
]
};4. Test the API
curl -X POST http://localhost:1337/api/audience/identify \
-H "Content-Type: application/json" \
-d '{
"lat": 40.7128,
"lng": -74.0060,
"timestamp": "2026-02-05T18:45:00Z"
}'Response:
{
"success": true,
"coordinates": {
"lat": 40.7128,
"lng": -74.0060,
"zipCode": "10000"
},
"geohashID": "dr5regw",
"daypart": {
"name": "Evening Peak"
},
"audiences": [
{
"id": "AUD-06",
"name": "Financial / Work / Business"
}
]
}NYC Geo Targeting
Three-level geo targeting for New York City: DMA (dissolved NYC boundary), Borough (5 boroughs), and Neighborhood (NYC Planning NTA polygons). Usable from Node/Express/Koa/Strapi and from React Native via your backend.
Works immediately after install (no init, no sync)
The package ships with full NYC geo data inside the package. You do not need to run sync or write any files. Install the package and matchPoint works for all of NYC:
- 1 DMA (dissolved NYC boundary)
- 5 boroughs (Manhattan, Brooklyn, Queens, The Bronx, Staten Island)
- 32 Manhattan neighborhoods (NTAs, code prefix
MN)
const { matchPoint } = require('@mrazakassar/audience-identifier-core');
// No setup – works right after npm install for all of NYC
const result = await matchPoint({ lat: 40.758, lng: -73.9855 });
// → { dma: { id: 'NYC', ... }, borough: { id: 'Manhattan', ... }, neighborhood: { id: 'MN0502', ... } }
const brooklyn = await matchPoint({ lat: 40.6942, lng: -73.9866 });
// → { dma: { id: 'NYC', ... }, borough: { id: 'Brooklyn', ... } }
const outside = await matchPoint({ lat: 34.05, lng: -118.24 });
// → {} (point outside NYC)- Default: Reads from bundled data (full NYC). No
dataDir, nosyncNYCData(), no init. - Optional: Run
syncNYCData()only if you want to refresh data from NYC Open Data and persist to./.geo/nyc; then pass{ dataDir: './.geo/nyc' }tomatchPointto use that copy.
DMA meaning (NYC): For NYC, DMA is a logical top-level region defined as the dissolved boundary of all five NYC boroughs (one polygon). This is not Nielsen DMA data—it is our internal geographic hierarchy for targeting.
Neighborhood scope: Neighborhood layer is Manhattan NTAs only (32 areas); other boroughs return dma + borough only.
Architecture (text overview)
┌─────────────────────────────────────────────────────────────────┐
│ Data sources (ArcGIS REST → GeoJSON) │
│ NYC_BOROUGH_GEOJSON_URL, NYC_NTA_GEOJSON_URL │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ syncNYCData() │
│ • Fetch Borough + NTA GeoJSON │
│ • Filter NTAs to Manhattan (prefix MN) for MVP │
│ • Dissolve 5 boroughs → 1 NYC DMA polygon (@turf/union) │
│ • Persist: .geo/nyc/index.json, dma/, borough/, neighborhood/ │
└───────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Query API (file storage default) │
│ listGeos(layer, filter) │ getGeo │ getGeometry │
│ matchPoint({lat,lng}) → bbox prefilter → booleanPointInPolygon │
│ validateTargeting({dmaId, boroughId, neighborhoodId}) │
│ In-memory LRU geometry cache │
└─────────────────────────────────────────────────────────────────┘Setup (env vars for data sources)
ArcGIS REST endpoints return GeoJSON. Set these optional env vars; if unset, the package uses built-in placeholder URLs (replace with your ArcGIS query URLs that return GeoJSON):
export NYC_BOROUGH_GEOJSON_URL="https://.../FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=geojson"
export NYC_NTA_GEOJSON_URL="https://.../FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=geojson"Placeholder defaults are in src/geo/nyc/config.js; document where to paste real URLs there or in your .env.
Run sync (download and persist data)
Sync downloads Borough + NTA GeoJSON, normalizes properties, filters NTAs to Manhattan only (prefix MN) for MVP, computes the NYC DMA boundary (union of boroughs via @turf/union), and writes file-based storage.
const { syncNYCData } = require('@mrazakassar/audience-identifier-core');
(async () => {
const result = await syncNYCData();
// { counts: { dma: 1, borough: 5, neighborhood: ~27 (Manhattan NTAs) }, updatedAt, dataDir, elapsedMs }
console.log(result);
})();Optional: pass dataDir, boroughGeoJsonUrl, ntaGeoJsonUrl:
await syncNYCData({ dataDir: './.geo/nyc', boroughGeoJsonUrl: process.env.NYC_BOROUGH_GEOJSON_URL });Example usage in Node (Express/Koa)
const express = require('express');
const { listGeos, getGeometry, matchPoint, nycGeoRoutes } = require('@mrazakassar/audience-identifier-core');
const app = express();
app.use(express.json());
// Mount sample routes under /geo/nyc
app.use('/geo/nyc', nycGeoRoutes());
// Or use the APIs directly:
app.get('/geo/dmas', async (req, res) => {
const list = await listGeos('dma');
res.json(list);
});
app.get('/geo/boroughs', async (req, res) => {
const list = await listGeos('borough', { dmaId: 'NYC' });
res.json(list);
});
app.get('/geo/neighborhoods', async (req, res) => {
const list = await listGeos('neighborhood', { boroughId: req.query.borough || 'Manhattan' });
res.json(list);
});
app.get('/geo/geometry/:layer/:id', async (req, res) => {
const geom = await getGeometry(req.params.layer, req.params.id);
if (!geom) return res.status(404).json({ error: 'Not found' });
res.json(geom);
});
app.post('/geo/matchPoint', async (req, res) => {
const { lat, lng } = req.body;
const result = await matchPoint({ lat, lng });
res.json(result);
});Example from React Native (calling backend)
Call your backend that uses this package; do not call sync or file I/O from RN.
// In React Native: call your backend
const matchNYCPoint = async (lat, lng) => {
const res = await fetch('https://your-api.com/geo/nyc/matchPoint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ lat, lng }),
});
return res.json();
};
// Result: { dma?: { id, name, layer }, borough?: { id, name, layer, parentId }, neighborhood?: { id, name, layer, parentId } }Data directory (file storage)
- Default (no options): The package uses bundled full NYC data inside the package (
data/geo-nyc): 1 DMA, 5 boroughs, 32 Manhattan neighborhoods. No files to create;matchPoint({ lat, lng })works for all of NYC as soon as you install. - Optional custom data: Run
syncNYCData()only to refresh from NYC Open Data; pass{ dataDir: './.geo/nyc' }to use that copy. Structure underdataDir:index.json– metadata, counts, list of items per layer withbbox.dma/NYC.geojson– dissolved NYC boundary.borough/<BoroughId>.geojson– one per borough.neighborhood/<NTAId>.geojson– Manhattan NTAs (e.g. MN01, MN02, …).
API summary
| Function | Description |
|----------|-------------|
| syncNYCData(options?) | Download Borough + NTA GeoJSON, normalize, dissolve DMA, persist. Idempotent. |
| listGeos(layer, filter?, options?) | layer: "dma" | "borough" | "neighborhood". Filter: dmaId, boroughId, prefix (e.g. "MN"). |
| getGeo(layer, id, options?) | Metadata for one id (no geometry). |
| getGeometry(layer, id, options?) | GeoJSON Feature for map/backend geo ops. |
| matchPoint({ lat, lng }, options?) | Point-in-polygon → { dma?: {id,name,layer}, borough?: {id,name,layer,parentId}, neighborhood?: {id,name,layer,parentId} }. Uses bbox prefilter and in-memory LRU geometry cache. |
| validateTargeting({ dmaId?, boroughId?, neighborhoodId? }, options?) | Returns { valid, errors } for hierarchy consistency. |
| nycGeoRoutes() | Express router: GET /dmas, /boroughs, /neighborhoods, /geometry/:layer/:id, POST /matchPoint, /validate. |
Extending to other cities/layers
- Keep the same layer pattern:
dma→borough→neighborhood(or city-specific equivalents). - Add a new config module (e.g.
src/geo/la/config.js) and reuselistGeos/getGeometry/matchPointpatterns with a differentdataDirand sync that writes the same index + geometry file layout. - Optional PostGIS: use
sql/nyc_geo_postgis.sqlas a template; implement a storage adapter that reads/writes fromgeo_dma/geo_borough/geo_neighborhoodwhenconfig.storage === 'db'. File storage remains the default and works without PostGIS.
Testing NYC Geo Targeting
npm run test:geo-nycTests cover: sync (1 DMA, 5 boroughs, Manhattan NTA count > 20 when endpoints available), listGeos with filters, getGeo/getGeometry, matchPoint for a known Manhattan point (40.7580, -73.9855) and outside NYC (34.0522, -118.2437), and validateTargeting (valid hierarchy and invalid borough/neighborhood mismatch). Without network, tests use a minimal fixture so the suite still passes.
Testing
# Run basic audience identification tests
npm test
# Run batch processing tests
npm run test:batch
# Test worldwide reverse geocoding (19 global locations)
npm run test:geocoding
# Test geohash generation
npm run test:geohash
# Test automatic geohash with real queries
npm run test:auto-geohash
# NYC Geo Targeting (sync, listGeos, matchPoint, validateTargeting)
npm run test:geo-nycPerformance
- Single Query: ~5-15ms (with cache hits faster)
- Batch (1000 records): ~10-20 seconds
- Caching: ZIP-to-zone lookups cached; reverse geocode responses cached (TTL 24h); NYC geometry LRU cache for matchPoint
- Nominatim: Use server-side only; cache reduces repeated calls for same coordinates
Troubleshooting
No audiences matched?
- Ensure your ZIP code is correct
- Check if ZIP code is supported (must be in supported cities)
- Review the
geoZonesarray - should not be empty
Wrong daypart returned?
- Timestamps must be in valid ISO format or Date objects
- Check timezone: internally uses UTC
- Verify daypart times in exported
daypartsarray
Commuter audience not matching?
- Requires
requiresWeekday: true(Monday-Friday only) - Requires BOTH startZone and endZone
- Start must be residential, end must be work zone
- Must be during Early Morning (5-9 AM) or Evening Peak (4-7 PM)
License
MIT
Version: 1.3.2
Last Updated: February 2026
Highlights: Worldwide reverse geocoding, auto geohash generation, zero-config setup
