Mongolian address & zipcode intelligence toolkit
Offline lookup · fuzzy search · OSM geocoding · hybrid resolver · interactive map
- Offline-first — 2600+ zipcodes built in, no API keys, no network required
- Fuzzy search — Cyrillic & Latin, typo-tolerant
- Autocomplete — prefix-biased suggestions for search UIs
- OSM geocoding — optional forward & reverse geocoding via Nominatim
- Hybrid resolver — local dataset + OSM fallback with confidence scoring
- React hooks —
useZipcodeSearch,useResolveAddress,useReverseGeocode - Map component — drop-in
<ZipcodeMap />with click-to-resolve - TypeScript — full type definitions
- ESM + CJS — works everywhere
npm install mnzipcodeimport { lookup, isValid, search, suggest, resolve } from 'mnzipcode'
lookup('11000')
// → { resolved: true, zipcode: '11000', source: 'local', confidence: 1,
// normalized: { country: 'Mongolia', city: 'Ulaanbaatar' } }
isValid('11000') // true
isValid('99999') // false
search('Баянзүрх')
// → [{ zipcode: '13000', normalized: { city: 'Ulaanbaatar', district: 'Bayanzurkh' }, ... }]
suggest('Дор', { limit: 3 })
// → [{ name: 'Dornod', zipcode: '21000' }, { name: 'Dornogovi', zipcode: '44000' }, ...]
await resolve('Сүхбаатар дүүрэг', { mode: 'hybrid' })
// → { resolved: true, zipcode: '14000', source: 'hybrid', confidence: 0.9, ... }Exact zipcode lookup. Returns ResolveResult | null.
lookup('12001')
// → { zipcode: '12001', normalized: { country: 'Mongolia', city: 'Ulaanbaatar',
// district: 'Baganuur', subdistrict: 'Хэрлэн голын хөвөө-1' } }
lookup(21000) // numeric input works
lookup('99999') // nullReturns true if the zipcode exists in the dataset.
Fuzzy search across all Cyrillic and Latin names.
search('Баянзүрх', { limit: 5 })
search('Dornod')
search('Дорнот') // typo-tolerant| Option | Type | Default | Description |
|---|---|---|---|
limit |
number |
10 |
Max results |
Prefix-biased autocomplete suggestions.
suggest('Дор', { limit: 3 })
// → [{ name: 'Dornod', zipcode: '21000', confidence: 0.9 }, ...]Forward and reverse geocoding via Nominatim. Opt-in — only called when you use them.
import { geocode } from 'mnzipcode'
await geocode('Ulaanbaatar', { userAgent: 'my-app/1.0' })
// → { resolved: true, source: 'osm', normalized: { city: 'Ulaanbaatar', lat: 47.9212, lon: 106.9057 } }import { reverse } from 'mnzipcode'
await reverse(47.9212, 106.9057)
// → { resolved: true, source: 'osm', normalized: { city: 'Ulaanbaatar', district: 'Sukhbaatar' } }| Option | Type | Default | Description |
|---|---|---|---|
endpoint |
string |
Nominatim public | Custom Nominatim instance |
userAgent |
string |
mnzipcode/2.0 |
User-Agent header |
cache |
boolean |
true |
In-memory caching |
cacheTTL |
number |
3600000 |
Cache TTL in ms (1h) |
countryCode |
string |
mn |
Country filter |
Combines local dataset + OSM into one unified call with confidence scoring.
import { resolve } from 'mnzipcode'
await resolve('11000', { mode: 'local' }) // offline, instant
await resolve('Peace Avenue', { mode: 'osm' }) // OSM only
await resolve('Баянзүрх', { mode: 'hybrid' }) // local first, OSM fallbackHow hybrid mode works:
Input
├─ looks like zipcode? → exact lookup → done
├─ fuzzy search local → confident match? → done
├─ fall back to OSM geocoding
├─ cross-reference with local data for zipcode
└─ return unified result with source: 'hybrid'
| Option | Type | Default |
|---|---|---|
mode |
'local' | 'osm' | 'hybrid' |
'hybrid' |
osm |
OsmOptions |
— |
fuzzyThreshold |
number |
0.6 |
Every function returns a consistent ResolveResult:
interface ResolveResult {
input: string
resolved: boolean
zipcode?: string
confidence: number // 0–1
source: 'local' | 'osm' | 'hybrid'
normalized?: {
country?: string
city?: string
district?: string
subdistrict?: string
khoroo?: string
aimag?: string // province
soum?: string // sub-province
lat?: number
lon?: number
rawAddress?: string
}
candidates?: Array<{
name: string
zipcode?: string
source: 'local' | 'osm'
confidence: number
}>
}Optional hooks and components via subpath imports. React is not required for the core package.
npm install mnzipcode reactDebounced offline search for building search UIs.
import { useZipcodeSearch } from 'mnzipcode/react'
function ZipcodeSearch() {
const [query, setQuery] = useState('')
const { results, isSearching } = useZipcodeSearch(query, { debounceMs: 300, limit: 10 })
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." />
{results.map((r) => (
<div key={r.zipcode}>{r.zipcode} — {r.normalized?.district}</div>
))}
</div>
)
}Hybrid resolver with loading and error states.
import { useResolveAddress } from 'mnzipcode/react'
function AddressResolver() {
const [input, setInput] = useState('')
const { result, isLoading, error } = useResolveAddress(input, { mode: 'hybrid' })
return (
<div>
<input value={input} onChange={(e) => setInput(e.target.value)} />
{result?.resolved && <p>Zipcode: {result.zipcode} ({result.confidence * 100}%)</p>}
</div>
)
}Coordinates to address + zipcode. Triggers when coordinates change.
import { useReverseGeocode } from 'mnzipcode/react'
function MapClickResult() {
const [coords, setCoords] = useState(null)
const { result, isLoading } = useReverseGeocode(coords?.lat ?? null, coords?.lon ?? null)
// On map click → setCoords({ lat, lon })
// result.zipcode → resolved zipcode
}Drop-in interactive map with click-to-resolve. Requires leaflet and react-leaflet as peer dependencies.
npm install mnzipcode leaflet react-leafletimport { ZipcodeMap } from 'mnzipcode/map'
import 'leaflet/dist/leaflet.css'
function App() {
return (
<ZipcodeMap
height={500}
zoom={12}
center={[47.92, 106.91]}
onResolve={(result, coords) => {
console.log(result.zipcode, result.normalized)
}}
/>
)
}Click anywhere on the map → reverse geocode → zipcode + structured address.
| Prop | Type | Default | Description |
|---|---|---|---|
height |
string | number |
'400px' |
Map height |
width |
string | number |
'100%' |
Map width |
center |
[number, number] |
Mongolia | Initial center [lat, lon] |
zoom |
number |
6 |
Initial zoom level |
className |
string |
— | CSS class for wrapper div |
style |
CSSProperties |
— | Inline style for wrapper |
mapClassName |
string |
— | CSS class for map container |
mapStyle |
CSSProperties |
— | Inline style for map |
tileUrl |
string |
OSM default | Custom tile URL |
tileAttribution |
string |
OSM | Tile attribution |
onResolve |
(result, coords) => void |
— | Called when location resolved |
onClick |
(coords) => void |
— | Called on map click |
renderPopup |
(result, loading, coords) => ReactNode |
— | Custom popup content |
markerIcon |
L.Icon |
Default pin | Custom marker icon |
disabled |
boolean |
false |
Disable click interaction |
If you prefer full control over the map instead of <ZipcodeMap />:
import { MapContainer, TileLayer, useMapEvents, Marker, Popup } from 'react-leaflet'
import { useReverseGeocode } from 'mnzipcode/react'
function LocationPicker() {
const [coords, setCoords] = useState(null)
const { result } = useReverseGeocode(coords?.lat ?? null, coords?.lon ?? null)
function ClickHandler() {
useMapEvents({ click(e) { setCoords({ lat: e.latlng.lat, lon: e.latlng.lng }) } })
return null
}
return (
<MapContainer center={[47.92, 106.91]} zoom={12} style={{ height: 400 }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ClickHandler />
{coords && (
<Marker position={[coords.lat, coords.lon]}>
<Popup>{result?.normalized?.rawAddress}</Popup>
</Marker>
)}
</MapContainer>
)
}| Import | What you get | Requires |
|---|---|---|
mnzipcode |
Core: lookup, isValid, search, suggest, resolve, geocode, reverse |
— |
mnzipcode/react |
Hooks: useZipcodeSearch, useResolveAddress, useReverseGeocode |
react |
mnzipcode/map |
Component: <ZipcodeMap /> |
react, leaflet, react-leaflet |
All peer dependencies are optional — install only what you use.
const { lookup, isValid, search } = require('mnzipcode')
lookup('11000')
isValid('21000')
search('Dornod')2600+ Mongolian zipcodes covering all administrative levels:
| Level | Examples | Count |
|---|---|---|
| Capital & Aimags | Ulaanbaatar, Dornod, Khentii | 22 |
| Districts & Soums | Bayanzurkh, Baganuur, Khalkhgol | 340+ |
| Sub-areas & Bags | Villages, neighborhoods, settlements | 2200+ |
Structure: Province/Capital > District/Soum > Sub-area
Each entry includes Cyrillic name. Top-level entries include Latin transliteration.
git clone https://github.com/bekkaze/mnzipcode.git
cd mnzipcode
npm install
npm test
npm run build