Complete Bangladesh geographical data for every JavaScript & TypeScript project
Divisions · Districts · Upazilas · Unions · Postcodes — bilingual (EN / বাংলা)
Installation · Quick Start · API Reference · Framework Guides · React Components · Types · Contributing
bd-geo-info provides structured, hierarchical geographical data for Bangladesh. The core is 100% framework-agnostic — it works in React, Vue, Angular, Svelte, NestJS, Express, Next.js, plain Node.js, Bun, Deno, or anywhere JavaScript runs.
React UI components are available as an opt-in subpath (bd-geo-info/react) so non-React projects never pay the cost of pulling in React.
- Zero required dependencies — React is optional, not forced
- O(1) hash-map lookups — datasets indexed by parent ID, no linear scans
- Code-split by default — large datasets load on demand, not upfront
- Build-time JSON minification — ~300 KB saved on every install
- Dual CJS + ESM builds — works with every bundler and module system
- Bilingual — English and বাংলা for all data points
- Full TypeScript — strict types for all data shapes and component props
# npm
npm install bd-geo-info
# yarn
yarn add bd-geo-info
# pnpm
pnpm add bd-geo-info
# bun
bun add bd-geo-infoReact components need React as a peer dependency:
npm install bd-geo-info react react-domimport { getDivisions, getDistricts, getUpazilas, getUnions } from 'bd-geo-info';
// 1. All divisions
const divisions = await getDivisions('en');
// [{ value: '1', label: 'Chittagong' }, { value: '2', label: 'Rajshahi' }, ...]
// 2. Districts for a division
const districts = await getDistricts('3', 'en');
// [{ value: '17', label: 'Dhaka' }, ...]
// 3. Upazilas for a district
const upazilas = await getUpazilas('17', 'en');
// [{ value: '101', label: 'Mirpur', id: '101', district_id: '17', ... }, ...]
// 4. Unions for an upazila
const unions = await getUnions('101', 'en');
// [{ value: '1001', label: 'Pallabi' }, ...]
// 5. Bangla output
const divisionsBn = await getDivisions('bn');
// [{ value: '1', label: 'চট্টগ্রাম' }, ...]All functions are exported from the main entry point with no framework dependency.
import {
getDivisions, getDistricts, getUpazilas, getUnions,
getPostCodes, getPostCode, formatAddress, validatePostCode
} from 'bd-geo-info';Returns all 8 divisions of Bangladesh.
getDivisions(language?: 'en' | 'bn'): Promise<{ value: string; label: string }[]>const divisions = await getDivisions('en');
// [{ value: '1', label: 'Chittagong' }, { value: '2', label: 'Rajshahi' }, ...]
const divisionsBn = await getDivisions('bn');
// [{ value: '1', label: 'চট্টগ্রাম' }, ...]Returns all districts belonging to a division.
getDistricts(divisionId: string, language?: 'en' | 'bn'): Promise<{ value: string; label: string }[]>const districts = await getDistricts('3', 'en');
// [{ value: '17', label: 'Dhaka' }, { value: '26', label: 'Gazipur' }, ...]Returns all upazilas belonging to a district. Includes full data fields alongside value / label.
getUpazilas(
districtId: string,
language?: 'en' | 'bn'
): Promise<{ id: string; district_id: string; name: string; bn_name: string; value: string; label: string; url?: string }[]>const upazilas = await getUpazilas('17', 'en');
// [{ id: '101', district_id: '17', name: 'Mirpur', value: '101', label: 'Mirpur', ... }, ...]Returns all unions belonging to an upazila.
getUnions(upazilaId: string, language?: 'en' | 'bn'): Promise<{ value: string; label: string; url?: string }[]>const unions = await getUnions('101', 'en');
// [{ value: '1001', label: 'Pallabi', url: '' }, ...]Async postcode lookup by district, optionally filtered by upazila name.
getPostCodes(districtId: string | null, upazila?: string): Promise<PostCode[]>const codes = await getPostCodes('17');
// [{ division_id: '3', district_id: '17', upazila: 'Mirpur', postOffice: 'Pallabi', postCode: '1216' }, ...]
const filtered = await getPostCodes('17', 'Mirpur');Synchronous postcode lookup. Useful when you need a result inline without await.
getPostCode(params: { division?: string; district?: string; upazila?: string } | null): PostCode[]import { getPostCode } from 'bd-geo-info';
const codes = getPostCode({ district: '17', upazila: 'Mirpur' });
// [{ postCode: '1216', postOffice: 'Pallabi', upazila: 'Mirpur', ... }, ...]Resolves IDs to names and returns a formatted address string.
formatAddress(
division: string,
district: string,
upazila: string,
union: string,
postCode: string,
street: string,
language?: 'en' | 'bn'
): Promise<string>const address = await formatAddress('3', '17', '101', '1001', '1216', 'House 5, Road 3', 'en');
// "House 5, Road 3, Pallabi, Mirpur, Dhaka-1216, Dhaka"
const addressBn = await formatAddress('3', '17', '101', '1001', '1216', '', 'bn');
// "পল্লবী, মিরপুর, ঢাকা-১২১৬, ঢাকা"Returns true if the postcode is a valid 4-digit Bangladesh postcode.
validatePostCode(postCode: string): booleanvalidatePostCode('1216'); // true
validatePostCode('12'); // false
validatePostCode('abcd'); // falseUse utility functions directly, or pull in the pre-built components from bd-geo-info/react.
Utilities only (custom UI):
import { useState, useEffect } from 'react';
import { getDivisions, getDistricts } from 'bd-geo-info';
export function AddressSelector() {
const [divisions, setDivisions] = useState<{ value: string; label: string }[]>([]);
const [districts, setDistricts] = useState<{ value: string; label: string }[]>([]);
const [divisionId, setDivisionId] = useState('');
useEffect(() => {
getDivisions('en').then(setDivisions);
}, []);
useEffect(() => {
if (divisionId) getDistricts(divisionId, 'en').then(setDistricts);
else setDistricts([]);
}, [divisionId]);
return (
<>
<select value={divisionId} onChange={e => setDivisionId(e.target.value)}>
<option value="">Select Division</option>
{divisions.map(d => <option key={d.value} value={d.value}>{d.label}</option>)}
</select>
<select disabled={!divisionId}>
<option value="">Select District</option>
{districts.map(d => <option key={d.value} value={d.value}>{d.label}</option>)}
</select>
</>
);
}Next.js — API Route (App Router):
// app/api/districts/route.ts
import { getDistricts } from 'bd-geo-info';
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const divisionId = searchParams.get('divisionId') ?? '';
const lang = (searchParams.get('lang') ?? 'en') as 'en' | 'bn';
const districts = await getDistricts(divisionId, lang);
return Response.json(districts);
}<script setup lang="ts">
import { ref, watch, onMounted } from 'vue';
import { getDivisions, getDistricts, getUpazilas } from 'bd-geo-info';
const divisions = ref<{ value: string; label: string }[]>([]);
const districts = ref<{ value: string; label: string }[]>([]);
const upazilas = ref<{ value: string; label: string }[]>([]);
const selectedDivision = ref('');
const selectedDistrict = ref('');
onMounted(async () => {
divisions.value = await getDivisions('en');
});
watch(selectedDivision, async (id) => {
selectedDistrict.value = '';
districts.value = id ? await getDistricts(id, 'en') : [];
upazilas.value = [];
});
watch(selectedDistrict, async (id) => {
upazilas.value = id ? await getUpazilas(id, 'en') : [];
});
</script>
<template>
<select v-model="selectedDivision">
<option value="">Select Division</option>
<option v-for="d in divisions" :key="d.value" :value="d.value">{{ d.label }}</option>
</select>
<select v-model="selectedDistrict" :disabled="!selectedDivision">
<option value="">Select District</option>
<option v-for="d in districts" :key="d.value" :value="d.value">{{ d.label }}</option>
</select>
<select :disabled="!selectedDistrict">
<option value="">Select Upazila</option>
<option v-for="u in upazilas" :key="u.value" :value="u.value">{{ u.label }}</option>
</select>
</template>Service:
// geo.service.ts
import { Injectable } from '@angular/core';
import { from } from 'rxjs';
import { getDivisions, getDistricts, getUpazilas, getUnions } from 'bd-geo-info';
@Injectable({ providedIn: 'root' })
export class GeoService {
getDivisions(lang: 'en' | 'bn' = 'en') { return from(getDivisions(lang)); }
getDistricts(id: string, lang: 'en' | 'bn' = 'en') { return from(getDistricts(id, lang)); }
getUpazilas(id: string, lang: 'en' | 'bn' = 'en') { return from(getUpazilas(id, lang)); }
getUnions(id: string, lang: 'en' | 'bn' = 'en') { return from(getUnions(id, lang)); }
}Component:
// address.component.ts
import { Component, OnInit } from '@angular/core';
import { GeoService } from './geo.service';
@Component({
selector: 'app-address',
template: `
<select (change)="onDivisionChange($event)">
<option value="">Select Division</option>
<option *ngFor="let d of divisions" [value]="d.value">{{ d.label }}</option>
</select>
<select (change)="onDistrictChange($event)" [disabled]="!districts.length">
<option value="">Select District</option>
<option *ngFor="let d of districts" [value]="d.value">{{ d.label }}</option>
</select>
<select [disabled]="!upazilas.length">
<option value="">Select Upazila</option>
<option *ngFor="let u of upazilas" [value]="u.value">{{ u.label }}</option>
</select>
`
})
export class AddressComponent implements OnInit {
divisions: { value: string; label: string }[] = [];
districts: { value: string; label: string }[] = [];
upazilas: { value: string; label: string }[] = [];
constructor(private geo: GeoService) {}
ngOnInit() {
this.geo.getDivisions('en').subscribe(data => this.divisions = data);
}
onDivisionChange(e: Event) {
const id = (e.target as HTMLSelectElement).value;
this.districts = []; this.upazilas = [];
if (id) this.geo.getDistricts(id, 'en').subscribe(data => this.districts = data);
}
onDistrictChange(e: Event) {
const id = (e.target as HTMLSelectElement).value;
this.upazilas = [];
if (id) this.geo.getUpazilas(id, 'en').subscribe(data => this.upazilas = data);
}
}<script lang="ts">
import { onMount } from 'svelte';
import { getDivisions, getDistricts, getUpazilas, getUnions } from 'bd-geo-info';
let divisions: { value: string; label: string }[] = [];
let districts: { value: string; label: string }[] = [];
let upazilas: { value: string; label: string }[] = [];
let unions: { value: string; label: string }[] = [];
let divisionId = '', districtId = '', upazilaId = '';
onMount(async () => { divisions = await getDivisions('en'); });
async function onDivisionChange() {
districtId = ''; districts = []; upazilas = []; unions = [];
if (divisionId) districts = await getDistricts(divisionId, 'en');
}
async function onDistrictChange() {
upazilaId = ''; upazilas = []; unions = [];
if (districtId) upazilas = await getUpazilas(districtId, 'en');
}
async function onUpazilaChange() {
unions = [];
if (upazilaId) unions = await getUnions(upazilaId, 'en');
}
</script>
<select bind:value={divisionId} on:change={onDivisionChange}>
<option value="">Select Division</option>
{#each divisions as d}<option value={d.value}>{d.label}</option>{/each}
</select>
<select bind:value={districtId} on:change={onDistrictChange} disabled={!divisionId}>
<option value="">Select District</option>
{#each districts as d}<option value={d.value}>{d.label}</option>{/each}
</select>
<select bind:value={upazilaId} on:change={onUpazilaChange} disabled={!districtId}>
<option value="">Select Upazila</option>
{#each upazilas as u}<option value={u.value}>{u.label}</option>{/each}
</select>
<select disabled={!upazilaId}>
<option value="">Select Union</option>
{#each unions as u}<option value={u.value}>{u.label}</option>{/each}
</select>Service:
// geo/geo.service.ts
import { Injectable } from '@nestjs/common';
import { getDivisions, getDistricts, getUpazilas, getUnions, getPostCodes } from 'bd-geo-info';
import type { PostCode } from 'bd-geo-info';
@Injectable()
export class GeoService {
getDivisions(lang: 'en' | 'bn' = 'en') {
return getDivisions(lang);
}
getDistricts(divisionId: string, lang: 'en' | 'bn' = 'en') {
return getDistricts(divisionId, lang);
}
getUpazilas(districtId: string, lang: 'en' | 'bn' = 'en') {
return getUpazilas(districtId, lang);
}
getUnions(upazilaId: string, lang: 'en' | 'bn' = 'en') {
return getUnions(upazilaId, lang);
}
getPostCodes(districtId: string, upazila?: string): Promise<PostCode[]> {
return getPostCodes(districtId, upazila);
}
}Controller:
// geo/geo.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { GeoService } from './geo.service';
@Controller('geo')
export class GeoController {
constructor(private readonly geo: GeoService) {}
@Get('divisions')
getDivisions(@Query('lang') lang: 'en' | 'bn' = 'en') {
return this.geo.getDivisions(lang);
}
@Get('districts')
getDistricts(@Query('divisionId') divisionId: string, @Query('lang') lang: 'en' | 'bn' = 'en') {
return this.geo.getDistricts(divisionId, lang);
}
@Get('upazilas')
getUpazilas(@Query('districtId') districtId: string, @Query('lang') lang: 'en' | 'bn' = 'en') {
return this.geo.getUpazilas(districtId, lang);
}
@Get('unions')
getUnions(@Query('upazilaId') upazilaId: string, @Query('lang') lang: 'en' | 'bn' = 'en') {
return this.geo.getUnions(upazilaId, lang);
}
@Get('postcodes')
getPostCodes(@Query('districtId') districtId: string, @Query('upazila') upazila?: string) {
return this.geo.getPostCodes(districtId, upazila);
}
}import express from 'express';
import { getDivisions, getDistricts, getUpazilas, getUnions } from 'bd-geo-info';
const app = express();
app.get('/api/divisions', async (req, res) => {
const lang = (req.query.lang as 'en' | 'bn') ?? 'en';
res.json(await getDivisions(lang));
});
app.get('/api/districts', async (req, res) => {
const { divisionId, lang = 'en' } = req.query as { divisionId: string; lang: 'en' | 'bn' };
res.json(await getDistricts(divisionId, lang));
});
app.get('/api/upazilas', async (req, res) => {
const { districtId, lang = 'en' } = req.query as { districtId: string; lang: 'en' | 'bn' };
res.json(await getUpazilas(districtId, lang));
});
app.get('/api/unions', async (req, res) => {
const { upazilaId, lang = 'en' } = req.query as { upazilaId: string; lang: 'en' | 'bn' };
res.json(await getUnions(upazilaId, lang));
});
app.listen(3000);Import from bd-geo-info/react. Requires React ≥ 16.8.
import {
AddressForm,
DivisionSelect,
DistrictSelect,
UpazilaSelect,
UnionSelect,
} from 'bd-geo-info/react';A ready-to-use address form with cascading selects, validation, theming, and bilingual support.
import { AddressForm } from 'bd-geo-info/react';
import type { AddressFormData } from 'bd-geo-info/react';
export default function Page() {
const handleChange = (data: AddressFormData) => {
console.log(data);
// { division: 'Dhaka', district: 'Dhaka', upazila: 'Mirpur', union: 'Pallabi', postCode: '1216' }
};
return (
<AddressForm
language="en"
onChange={handleChange}
showPostCode
showUnion
validation={{
division: { required: true },
district: { required: true },
upazila: { required: true },
}}
customLabels={{
division: 'Division *',
district: 'District *',
upazila: 'Upazila *',
union: 'Union',
}}
theme={{
colors: { primary: '#2563eb', background: '#fff', border: '#d1d5db' },
borderRadius: '0.5rem',
fontSize: { input: '0.875rem' },
spacing: { input: '0.625rem 0.875rem' },
}}
/>
);
}AddressForm props:
| Prop | Type | Default | Description |
|---|---|---|---|
language |
'en' | 'bn' |
'en' |
Display language |
onChange |
(data: AddressFormData) => void |
— | Fires on every selection change |
onSubmit |
(data: AddressFormData) => void |
— | Fires on form submit |
showPostCode |
boolean |
true |
Show postcode field |
showUnion |
boolean |
true |
Show union selector |
showLabels |
boolean |
true |
Show field labels |
validation |
AddressFormValidation |
— | Per-field validation rules |
customLabels |
AddressFormLabels |
— | Override default label text |
customErrors |
AddressFormErrors |
— | Override or inject error messages |
theme |
Theme |
— | Custom color / spacing / radius theme |
className |
string |
'' |
Class on the form container |
containerClassName |
string |
'' |
Class on the outer wrapper |
labelClassName |
string |
'' |
Class applied to labels |
errorClassName |
string |
'' |
Class applied to error messages |
inputContainerClassName |
string |
'' |
Class applied to each input wrapper |
disabled |
boolean |
false |
Disable all fields |
language |
'en' | 'bn' |
'en' |
Display language |
All four selects share a common set of props. They can be composed freely for custom layouts.
import { useState } from 'react';
import { DivisionSelect, DistrictSelect, UpazilaSelect, UnionSelect } from 'bd-geo-info/react';
import type { Division, District, Upazila, UnionData } from 'bd-geo-info/react';
export default function LocationPicker() {
const [division, setDivision] = useState<Division>();
const [district, setDistrict] = useState<District>();
const [upazila, setUpazila] = useState<Upazila>();
const [union, setUnion] = useState<UnionData>();
return (
<div className="grid grid-cols-2 gap-4">
<DivisionSelect
language="en"
value={division}
onChange={setDivision}
placeholder="Select Division"
customLabel="Division"
/>
<DistrictSelect
language="en"
division={division}
value={district}
onChange={setDistrict}
placeholder="Select District"
customLabel="District"
/>
<UpazilaSelect
language="en"
district={district}
value={upazila}
onChange={setUpazila}
placeholder="Select Upazila"
customLabel="Upazila"
/>
<UnionSelect
language="en"
upazila={upazila}
value={union}
onChange={setUnion}
placeholder="Select Union"
customLabel="Union"
/>
</div>
);
}Common select props:
| Prop | Type | Default | Description |
|---|---|---|---|
language |
'en' | 'bn' |
'en' |
Display language |
value |
data object | — | Controlled selected value |
onChange |
(value) => void |
— | Selection handler |
placeholder |
string |
— | Placeholder option text |
customLabel |
string | ReactNode |
— | Label rendered above the select |
customError |
string | ReactNode |
— | Error message rendered below |
disabled |
boolean |
false |
Disable the select |
className |
string |
'' |
Class on the <select> element |
containerClassName |
string |
'' |
Class on the wrapper div |
labelClassName |
string |
'' |
Class on the label |
errorClassName |
string |
'' |
Class on the error message |
theme |
Theme |
— | Custom theme object |
DistrictSelect also requires division?: Division and UpazilaSelect requires district?: District and UnionSelect requires upazila?: Upazila to load their dependent data.
Pass a theme object to any component or to AddressForm to override the default styles.
import type { Theme } from 'bd-geo-info';
const theme: Theme = {
colors: {
primary: '#7c3aed', // focus ring, active border
background: '#f9fafb', // select background
border: '#e5e7eb', // default border
},
borderRadius: '0.75rem',
fontSize: {
input: '0.9375rem',
},
spacing: {
input: '0.75rem 1rem', // padding inside select
label: '0 0 0.375rem', // margin below label
},
};Define per-field rules using ValidationRules. Custom validators return a string on failure or true on success.
import type { AddressFormValidation } from 'bd-geo-info';
const validation: AddressFormValidation = {
division: { required: true },
district: { required: true },
upazila: { required: true },
union: {
required: false,
customValidation: (value) => {
if (value === 'blocked-union-id') return 'Deliveries not available in this union';
return true;
},
},
};import type {
Division, // { id, name, bn_name, lat, long, url? }
District, // { id, division_id, name, bn_name, lat, long, url? }
Upazila, // { id, district_id, name, bn_name, url? }
UnionData, // { id, upazilla_id, name, bn_name, url }
Union, // { type, name?, database?, data?: UnionData[] }
PostCode, // { division_id, district_id?, district?, upazila, postOffice, postCode }
AddressFormData, // { division?, district?, upazila?, union?, postCode?, street? }
AddressFormValidation,
ValidationRules, // { required?, customValidation? }
Theme, // { colors?, borderRadius?, fontSize?, spacing? }
} from 'bd-geo-info';import type {
AddressFormProps,
AddressFormErrors,
AddressFormLabels,
SelectProps,
PostOfficeSelectProps,
} from 'bd-geo-info/react';All core types are also re-exported from bd-geo-info/react for convenience.
| Resource | Count | Notes |
|---|---|---|
| Divisions | 8 | All divisions with coordinates |
| Districts | 64 | All districts, linked to divisions |
| Upazilas | 495 | All upazilas, indexed by district |
| Unions | 4,554 | All unions, indexed by upazila |
| Post offices | 9,000+ | With postcodes, linked to district + upazila |
All data includes both English and বাংলা names.
O(1) lookups — upazilas and unions are stored in hash maps keyed by parent ID. Fetching upazilas for a district does not scan all 495 entries; it reads one key from a map.
Dynamic imports — getUnions and getUpazilas use import() so the large JSON files are not included in your initial bundle. They load only when called for the first time.
Tree-shaking — the package declares "sideEffects": false. Bundlers like webpack and Rollup will drop any function you do not import.
Minified JSON — all JSON assets are minified at build time (~300 KB saved vs raw source).
Contributions are welcome. Please read CONTRIBUTING.md before opening a pull request.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-change - Make your changes and run the build:
npm run build - Open a pull request against
main
Bug reports and data corrections belong in GitHub Issues.
MIT © Md Tanvir Ahamed Shanto
| mdtanvirahamedshanto@gmail.com | |
| GitHub | @mdtanvirahamedshanto |
| mdtanvirahamedshanto | |
| Portfolio | mdtanvirahamedshanto.vercel.app |