A lightweight library for navigating hierarchical location data (countries, states, cities, postal codes) with persistent state management. Works with Node.js and Bun.
npm install placrA lightweight, strongly-typed library for navigating hierarchical location data with persistent state management.


- Navigate through location hierarchies (countries, states, cities, postal codes)
- 17 navigation formats with strongly-typed returns
- Conditional navigation methods - loadNext() and loadPrevious() only appear when available
- Persistent state with SQLite (works offline)
- Built-in data for 250+ countries, 5000+ states, 150000+ cities
- Postal code support for US, CA, GB, DE, JP, FR, IN, AU, NL, IE
- Works with Node.js (v22+) and Bun
- TypeScript support out of the box
``bash`
npm install placr
`bash`
bun add placr
`typescript
import { Placr } from 'placr';
// Create a new instance - returns strongly typed Placr<'city-state-country'>
const nav = await Placr.create('city-state-country', 'US');
// Get the current location - nav.nav is typed as CityStateCountryNav
const current = nav.getNav();
console.log(current.nav);
// { city: 'New York', state: 'New York', stateShort: 'NY', country: 'US', countryShort: 'US' }
// Quick access via placeholder (address format)
console.log(current.placeholder);
// "New York, NY, US"
// Conditional navigation - loadNext() only exists when hasNext is true
if (current.hasNext) {
const next = current.loadNext();
}
// Mark current as complete
nav.markComplete();
`
Each format returns a specifically typed Nav object with only the fields for that format:
`typescript
// 'zip' format returns ZipNav
const zipNav = await Placr.create('zip', 'US');
const result = zipNav.getNav();
result.nav.zip; // ✓ string
result.nav.country; // ✓ string
result.nav.city; // ✗ TypeScript error - doesn't exist on ZipNav
// 'city-state-country' format returns CityStateCountryNav
const cityNav = await Placr.create('city-state-country', 'US');
const cityResult = cityNav.getNav();
cityResult.nav.city; // ✓ string
cityResult.nav.state; // ✓ string
cityResult.nav.stateShort; // ✓ string
cityResult.nav.country; // ✓ string
cityResult.nav.countryShort; // ✓ string
`
Navigation methods are only available when they can be used:
`typescript
const nav = await Placr.create('city-state', 'US');
const current = nav.getNav();
// TypeScript knows exactly what's available based on hasNext/hasPrevious
if (current.hasNext && current.hasPrevious) {
// Both methods available
current.loadNext(); // ✓
current.loadPrevious(); // ✓
} else if (current.hasNext) {
// Only loadNext available
current.loadNext(); // ✓
current.loadPrevious(); // ✗ TypeScript error
} else if (current.hasPrevious) {
// Only loadPrevious available
current.loadNext(); // ✗ TypeScript error
current.loadPrevious(); // ✓
}
`
| Format | Nav Type | Example Output |
|--------|----------|----------------|
| zip | ZipNav | { zip: '10001', country: 'US' } |zip-country
| | ZipCountryNav | { zip: '10001', country: 'US', countryShort: 'US' } |city
| | CityNav | { city: 'New York', country: 'US' } |city-state
| | CityStateNav | { city: 'New York', state: 'New York', stateShort: 'NY', country: 'US' } |city-state-country
| | CityStateCountryNav | { city: 'New York', state: 'New York', stateShort: 'NY', country: 'US', countryShort: 'US' } |state
| | StateNav | { state: 'New York', stateShort: 'NY', country: 'US' } |state-country
| | StateCountryNav | { state: 'New York', stateShort: 'NY', country: 'US', countryShort: 'US' } |county
| | CountyNav | { county: 'Kings County', country: 'US' } |
Combine locations with custom search queries:
| Format | Nav Type | Example Output |
|--------|----------|----------------|
| query | QueryNav | { query: 'restaurants', country: 'US' } |query-zip
| | QueryZipNav | { query: 'restaurants', zip: '10001', country: 'US' } |query-zip-country
| | QueryZipCountryNav | { query: 'restaurants', zip: '10001', country: 'US', countryShort: 'US' } |query-city
| | QueryCityNav | { query: 'restaurants', city: 'New York', country: 'US' } |query-city-state
| | QueryCityStateNav | { query: 'restaurants', city: 'New York', state: 'New York', stateShort: 'NY', country: 'US' } |query-city-state-country
| | QueryCityStateCountryNav | { query: 'restaurants', city: 'New York', state: 'New York', stateShort: 'NY', country: 'US', countryShort: 'US' } |query-state
| | QueryStateNav | { query: 'restaurants', state: 'New York', stateShort: 'NY', country: 'US' } |query-state-country
| | QueryStateCountryNav | { query: 'restaurants', state: 'New York', stateShort: 'NY', country: 'US', countryShort: 'US' } |query-county
| | QueryCountyNav | { query: 'restaurants', county: 'Kings County', country: 'US' } |
The placeholder field provides a quick, human-readable address string:
`typescript
// Location formats
const nav = await Placr.create('city-state-country', 'US');
nav.getNav().placeholder; // "New York, NY, US"
const zipNav = await Placr.create('zip-country', 'US');
zipNav.getNav().placeholder; // "10001, US"
// Query formats include the query
const queryNav = await Placr.create('query-city-state', 'US');
queryNav.addSearchQuery('restaurants');
queryNav.getNav().placeholder; // "restaurants in New York, NY"
`
Creates a new Placr instance with strongly-typed returns.
`typescript
// Each format returns a specifically typed Placr instance
const nav = await Placr.create('city-state-country', 'US');
// Type: Placr<'city-state-country'>
// With custom database path
const nav2 = await Placr.create('zip-country', 'all', './custom.db');
// Type: Placr<'zip-country'>
`
Parameters:
- format (NavFormat): Navigation format. Default: 'zip-country'targetCountry
- (ICountryShort | 'all'): ISO country code or 'all'. Default: 'US'dbPath
- (string): Path to SQLite database. Default: .nav.db
#### getNav(): NavResponse
Returns the current navigation item without advancing.
`typescript`
const current = nav.getNav();
// current.nav is typed based on the format
#### getNextNav(): NavResponse
Advances to and returns the next navigation item.
`typescript`
const next = nav.getNextNav();
#### getPreviousNav(): NavResponse
Goes back to and returns the previous navigation item.
`typescript`
const previous = nav.getPreviousNav();
#### markComplete(): void
Marks the current navigation item as completed.
`typescript`
nav.markComplete();
#### resetNav(): void
Resets navigation to the beginning.
`typescript`
nav.resetNav();
#### addSearchQueries(queries: string[]): void
Adds search queries for query-based navigation formats.
`typescript`
nav.addSearchQueries(['restaurants', 'hotels', 'attractions']);
#### addSearchQuery(query: string): void
Adds a single search query.
`typescript`
nav.addSearchQuery('coffee shops');
#### clearSearchQueries(): void
Removes all search queries.
`typescript`
nav.clearSearchQueries();
#### addCities(cities): void
Adds custom cities to the database.
`typescript`
nav.addCities([
{ city: 'Custom City', state: 'State Name', stateShort: 'ST', countryShort: 'US' }
]);
#### addStates(states): void
Adds custom states to the database.
`typescript`
nav.addStates([
{ state: 'Custom State', stateShort: 'CS', countryShort: 'US' }
]);
#### addCountry(countries): void
Adds custom countries to the database.
`typescript`
nav.addCountry([
{ country: 'Custom Country', countryShort: 'CC' }
]);
For navigation items with multiple pages:
#### setPageNav(totalPages: number, pages: Set
Sets pagination info for the current item.
`typescript`
nav.setPageNav(10, new Set([1])); // 10 total pages, starting at page 1
#### markPageAsDone(page: number): void
Marks a specific page as completed.
`typescript`
nav.markPageAsDone(1);
nav.markPageAsDone(2);
// Auto-completes when all pages are done
The response type is conditional based on navigation availability:
`typescript
// When hasNext and hasPrevious are both true
interface NavResponseBoth
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: true;
hasPrevious: true;
loadNext: () => NavResponse
loadPrevious: () => NavResponse
}
// When only hasNext is true
interface NavResponseNextOnly
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: true;
hasPrevious: false;
loadNext: () => NavResponse
}
// When only hasPrevious is true
interface NavResponsePreviousOnly
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: false;
hasPrevious: true;
loadPrevious: () => NavResponse
}
// When neither are available
interface NavResponseNone
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: false;
hasPrevious: false;
}
`
`typescript
// Location types
interface ZipNav { zip: string; country: string }
interface ZipCountryNav { zip: string; country: string; countryShort: string }
interface CityNav { city: string; country: string }
interface CityStateNav { city: string; state: string; stateShort: string; country: string }
interface CityStateCountryNav { city: string; state: string; stateShort: string; country: string; countryShort: string }
interface StateNav { state: string; stateShort: string; country: string }
interface StateCountryNav { state: string; stateShort: string; country: string; countryShort: string }
interface CountyNav { county: string; country: string }
// Query types
interface QueryNav { query: string; country: string }
interface QueryZipNav { query: string; zip: string; country: string }
interface QueryZipCountryNav { query: string; zip: string; country: string; countryShort: string }
interface QueryCityNav { query: string; city: string; country: string }
interface QueryCityStateNav { query: string; city: string; state: string; stateShort: string; country: string }
interface QueryCityStateCountryNav { query: string; city: string; state: string; stateShort: string; country: string; countryShort: string }
interface QueryStateNav { query: string; state: string; stateShort: string; country: string }
interface QueryStateCountryNav { query: string; state: string; stateShort: string; country: string; countryShort: string }
interface QueryCountyNav { query: string; county: string; country: string }
`
Maps format strings to their Nav types:
`typescript``
type NavTypeMap = {
'zip': ZipNav;
'zip-country': ZipCountryNav;
'city': CityNav;
'city-state': CityStateNav;
'city-state-country': CityStateCountryNav;
'state': StateNav;
'state-country': StateCountryNav;
'county': CountyNav;
'query': QueryNav;
'query-zip': QueryZipNav;
'query-zip-country': QueryZipCountryNav;
'query-city': QueryCityNav;
'query-city-state': QueryCityStateNav;
'query-city-state-country': QueryCityStateCountryNav;
'query-state': QueryStateNav;
'query-state-country': QueryStateCountryNav;
'query-county': QueryCountyNav;
}
Location data is sourced from:
- Countries States Cities Database - Countries, states, and cities
- GeoNames - Postal codes
Data is automatically downloaded on first use if not available.
| Runtime | Version | Status |
|---------|---------|--------|
| Node.js | 22+ | Supported |
| Bun | 1.0+ | Supported |
MIT
Contributions are welcome. Please open an issue or submit a pull request on GitHub.