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

@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.

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-core

Using yarn

yarn add @ivee/audience-identifier-core

Quick 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) required
  • lng (number): Longitude (-180 to 180) required
  • timestamp (Date|string|number): When the action occurred required
  • options (Object, optional):
    • rideMode (string): 'work', 'watch', 'shop'
    • startZone (Object): Start zone for commuter logic
    • endZone (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:

  1. Try OpenStreetMap (Nominatim) first for every call (US or non-US).
  2. On failure (network/timeout/error), try US cities fallback (sync, no network).
  3. 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): Latitude
  • lng (number): Longitude
  • options (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 Earth

Uses 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, no syncNYCData(), 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' } to matchPoint to 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 under dataDir:
    • index.json – metadata, counts, list of items per layer with bbox.
    • 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: dmaboroughneighborhood (or city-specific equivalents).
  • Add a new config module (e.g. src/geo/la/config.js) and reuse listGeos/getGeometry/matchPoint patterns with a different dataDir and sync that writes the same index + geometry file layout.
  • Optional PostGIS: use sql/nyc_geo_postgis.sql as a template; implement a storage adapter that reads/writes from geo_dma/geo_borough/geo_neighborhood when config.storage === 'db'. File storage remains the default and works without PostGIS.

Testing NYC Geo Targeting

npm run test:geo-nyc

Tests 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-nyc

Performance

  • 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 geoZones array - 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 dayparts array

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