Skip to content

bekkaze/mnzipcode

Repository files navigation


mnzipcode

Mongolian address & zipcode intelligence toolkit
Offline lookup · fuzzy search · OSM geocoding · hybrid resolver · interactive map

npm downloads size TypeScript license

ZipcodeMap component demo


Features

  • 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 hooksuseZipcodeSearch, useResolveAddress, useReverseGeocode
  • Map component — drop-in <ZipcodeMap /> with click-to-resolve
  • TypeScript — full type definitions
  • ESM + CJS — works everywhere

Install

npm install mnzipcode

Quick Start

import { 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, ... }

API Reference

Core (offline)

lookup(zipcode)

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') // null

isValid(zipcode)

Returns true if the zipcode exists in the dataset.

search(query, options?)

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

suggest(query, options?)

Prefix-biased autocomplete suggestions.

suggest('Дор', { limit: 3 })
// → [{ name: 'Dornod', zipcode: '21000', confidence: 0.9 }, ...]

OSM Geocoding (optional)

Forward and reverse geocoding via Nominatim. Opt-in — only called when you use them.

geocode(query, options?)

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

reverse(lat, lon, options?)

import { reverse } from 'mnzipcode'

await reverse(47.9212, 106.9057)
// → { resolved: true, source: 'osm', normalized: { city: 'Ulaanbaatar', district: 'Sukhbaatar' } }

OSM Options

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

Hybrid Resolver

Combines local dataset + OSM into one unified call with confidence scoring.

resolve(input, options?)

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 fallback

How 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

Output Schema

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

React / Next.js

Optional hooks and components via subpath imports. React is not required for the core package.

npm install mnzipcode react

useZipcodeSearch

Debounced 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>
  )
}

useResolveAddress

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>
  )
}

useReverseGeocode

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
}

Map Component

Drop-in interactive map with click-to-resolve. Requires leaflet and react-leaflet as peer dependencies.

npm install mnzipcode leaflet react-leaflet
import { 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.

<ZipcodeMap /> Props

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

With react-leaflet (manual)

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>
  )
}

Package Exports

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.


CommonJS

const { lookup, isValid, search } = require('mnzipcode')

lookup('11000')
isValid('21000')
search('Dornod')

Dataset

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.


Contributing

git clone https://github.com/bekkaze/mnzipcode.git
cd mnzipcode
npm install
npm test
npm run build

License

ISC

About

MNZipCode is a simple library for querying Mongolian zip codes.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors