@classytic/bd-areas
v1.1.0
Published
Bangladesh delivery areas with multi-provider support (RedX, Pathao, Steadfast) - 8 divisions, 64 districts, 2836+ areas
Readme
@classytic/bd-areas
Bangladesh delivery taxonomy as static data — zero network, zero API calls. Two entry points:
@classytic/bd-areas— unified shape (8 divisions, 64 districts, 2,836 areas with RedX IDs). Best for cascading address dropdowns built around the RedX area model.@classytic/bd-areas/pathao— Pathao-native taxonomy (64 cities, 1,258 zones). Best for Pathao-only flows (cascading dropdowns, CSV exports). ~80 KB instead of ~400 KB.
Both subpaths are independent — import only the one your UI needs and tree-shaking does the rest.
Install
npm install @classytic/bd-areasSubpath: @classytic/bd-areas (unified)
Used by RedX flows. Area records have a providers.redx numeric ID for every entry; providers.pathao and providers.steadfast are mostly empty (Pathao IDs don't fit the unified Area model — see the /pathao subpath instead).
import {
getDivisions, getDistrictsByDivision, getAreasByDistrict,
getArea, searchAreas, resolveArea,
} from '@classytic/bd-areas';
const divisions = getDivisions(); // 8 entries
const districts = getDistrictsByDivision('dhaka');
const areas = getAreasByDistrict('dhaka'); // [{ internalId, name, providers: { redx }, ... }]
const area = getArea(1206); // by internalId
const matches = searchAreas('mirpur'); // free-text, default limit 20
const full = resolveArea(1206); // adds full division + district objectsAPI summary
| Function | Purpose |
|---|---|
| getDivisions() / getDivisionById(id) / getDivisionByName(name) | 8 BD divisions |
| getDistrictsByDivision(divisionId) / getDistrictById(id) / getAllDistricts() | 64 districts |
| getArea(internalId) / getAllAreas() | 2,836 areas |
| getAreasByDistrict(id) / getAreasByDivision(id) / getAreasByPostCode(code) | filters |
| searchAreas(query, limit?) | free-text autocomplete |
| getAreaByProvider('redx', id) | inverse lookup |
| convertProviderId('redx', id, 'pathao') | cross-provider (Pathao mostly null today) |
| resolveArea(internalId) | area + full division + district |
| getStats() | dataset coverage report |
Types
interface Area {
internalId: number; // store this in your DB
name: string;
postCode: number | null;
zoneId: number; // internal zone (1-6) for tier pricing
districtId: string;
districtName: string;
divisionId: string;
divisionName: string;
providers: { redx?: number; pathao?: number; steadfast?: number };
}Subpath: @classytic/bd-areas/pathao (Pathao-native)
A static snapshot of merchant.pathao.com — exactly the data the Pathao web UI uses to populate its own dropdowns. Use this for any Pathao flow:
- Cascading city → zone dropdowns in checkout / address forms
- CSV bulk-upload exports (Pathao's CSV uses city + zone names, not IDs)
- Validating recipient city/zone before calling the Pathao API
import {
PATHAO_CITIES, // PathaoCity[] — alphabetical, all 64 cities
PATHAO_ZONES_BY_CITY, // Record<cityId, PathaoZone[]>
findCity, // by cityId
findCityByName, // case-insensitive, falls back to substring
getZonesByCity, // PathaoZone[] for a city
findZone, // (cityId, zoneId)
findZoneByName, // (cityId, name)
searchZones, // cross-city zone search, returns { ...zone, city }
getPathaoStats, // diagnostic counts per city
} from '@classytic/bd-areas/pathao';
// Cascading dropdown
const [cityId, setCityId] = useState<number>();
const zones = cityId ? getZonesByCity(cityId) : [];
// CSV row from address-with-IDs
const city = findCity(addr.cityId);
const zone = city ? findZone(city.cityId, addr.zoneId) : undefined;
csv.push({ RecipientCity: city?.cityName, RecipientZone: zone?.zoneName });Why a separate subpath?
- Granularity mismatch. Pathao zones are landmark/neighborhood-level (Gulshan, Banani) — they don't map 1:1 to the unified
Areashape (which is thana-level). Forcing them into one shape loses information. - Bundle weight. A Pathao-only checkout shouldn't ship 2,836 RedX areas (~400 KB unified) — the
/pathaosubpath is ~80 KB. - Update cadence. Pathao adds zones every few months; refreshing this subpath doesn't require touching the unified dataset.
Refresh: snapshot script
The /pathao data is regenerated by scripts/snapshot-pathao-merchant.mjs. Pathao changes infrequently — run this manually every few months and republish.
# 1. Get a fresh JWT from https://merchant.pathao.com (DevTools → any
# address-search request → copy `Authorization: Bearer <token>`)
# 2. Set in .env:
PATHAO_MERCHANT_TOKEN=<that token>
# 3. Run (resumable, hard-fails on rate limit, ~3 min):
node scripts/snapshot-pathao-merchant.mjs
# 4. Commit src/pathao/cities.ts + src/pathao/zones.ts + bump version.The snapshot writes after every city, so a mid-run rate-limit just resumes from _progress.json on the next run. Pass PATHAO_SNAPSHOT_MAX_CITIES=10 for a smoke run.
Tests
Three tiers per testing-infrastructure.md. Single vitest.config.ts with projects: [unit, integration, e2e].
| Command | What runs |
|---|---|
| npm test | unit + integration (default) — pure data assertions + cross-package shape tests |
| npm run test:unit | dataset shape, helpers |
| npm run test:integration | uses real RedXAdapter + composite resolver via mocked fetch — proves the dataset is shaped correctly for @classytic/carrier-bd |
| npm run test:e2e | env-gated live Pathao audit — reports drift between bd-areas and the live merchant API |
tests/unit and tests/integration MUST stay network-free.
Best practices
Storing addresses
// Unified (RedX) — store internalId
await saveAddress({ areaId: area.internalId, areaName: area.name, ... });
// Pathao — store cityId + zoneId
await saveAddress({ pathaoCityId: city.cityId, pathaoZoneId: zone.zoneId, ... });Calling carrier APIs (via @classytic/carrier-bd)
import { getArea } from '@classytic/bd-areas';
import { findZone } from '@classytic/bd-areas/pathao';
// RedX
const area = getArea(savedAddress.areaId);
await redxAdapter.buyLabel({ destination, packages, metadata: {
deliveryAreaId: area.providers.redx,
deliveryAreaName: area.name,
} }, ctx);
// Pathao
await pathaoAdapter.buyLabel({ destination, packages, metadata: {
deliveryCityId: savedAddress.pathaoCityId,
deliveryZoneId: savedAddress.pathaoZoneId,
} }, ctx);License
MIT © Classytic
