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

mnzipcode

v2.0.0

Published

Mongolian address and zipcode intelligence toolkit — offline lookup, search, OSM geocoding, and hybrid resolver

Readme


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