placr
v0.0.5
Published
A lightweight library for navigating hierarchical location data (countries, states, cities, postal codes) with persistent state management. Works with Node.js and Bun.
Maintainers
Readme
placr
A lightweight, strongly-typed library for navigating hierarchical location data with persistent state management.
Features
- Navigate through location hierarchies (countries, states, cities, postal codes)
- 17 navigation formats with strongly-typed returns
- Conditional navigation methods -
loadNext()andloadPrevious()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
Installation
npm install placrbun add placrQuick Start
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();Type-Safe Navigation
Each format returns a specifically typed Nav object with only the fields for that format:
// '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; // ✓ stringConditional Navigation Methods
Navigation methods are only available when they can be used:
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(); // ✓
}Navigation Formats
Location Formats
| 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' } |
Query-based Formats
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' } |
Placeholder Format
The placeholder field provides a quick, human-readable address string:
// 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"API Reference
Placr.create(format?, targetCountry?, dbPath?)
Creates a new Placr instance with strongly-typed returns.
// 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
Navigation Methods
getNav(): NavResponse<T> | null
Returns the current navigation item without advancing.
const current = nav.getNav();
// current.nav is typed based on the formatgetNextNav(): NavResponse<T> | null
Advances to and returns the next navigation item.
const next = nav.getNextNav();getPreviousNav(): NavResponse<T> | null
Goes back to and returns the previous navigation item.
const previous = nav.getPreviousNav();markComplete(): void
Marks the current navigation item as completed.
nav.markComplete();resetNav(): void
Resets navigation to the beginning.
nav.resetNav();Query Methods
addSearchQueries(queries: string[]): void
Adds search queries for query-based navigation formats.
nav.addSearchQueries(['restaurants', 'hotels', 'attractions']);addSearchQuery(query: string): void
Adds a single search query.
nav.addSearchQuery('coffee shops');clearSearchQueries(): void
Removes all search queries.
nav.clearSearchQueries();Data Methods
addCities(cities): void
Adds custom cities to the database.
nav.addCities([
{ city: 'Custom City', state: 'State Name', stateShort: 'ST', countryShort: 'US' }
]);addStates(states): void
Adds custom states to the database.
nav.addStates([
{ state: 'Custom State', stateShort: 'CS', countryShort: 'US' }
]);addCountry(countries): void
Adds custom countries to the database.
nav.addCountry([
{ country: 'Custom Country', countryShort: 'CC' }
]);Pagination Methods
For navigation items with multiple pages:
setPageNav(totalPages: number, pages: Set<number>): void
Sets pagination info for the current item.
nav.setPageNav(10, new Set([1])); // 10 total pages, starting at page 1markPageAsDone(page: number): void
Marks a specific page as completed.
nav.markPageAsDone(1);
nav.markPageAsDone(2);
// Auto-completes when all pages are doneTypes
NavResponse
The response type is conditional based on navigation availability:
// When hasNext and hasPrevious are both true
interface NavResponseBoth<T> {
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: true;
hasPrevious: true;
loadNext: () => NavResponse<T>;
loadPrevious: () => NavResponse<T>;
}
// When only hasNext is true
interface NavResponseNextOnly<T> {
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: true;
hasPrevious: false;
loadNext: () => NavResponse<T>;
}
// When only hasPrevious is true
interface NavResponsePreviousOnly<T> {
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: false;
hasPrevious: true;
loadPrevious: () => NavResponse<T>;
}
// When neither are available
interface NavResponseNone<T> {
nav: T;
placeholder: string;
page: PageNav | 'completed' | null;
hasNext: false;
hasPrevious: false;
}Format-Specific Nav Types
// 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 }NavTypeMap
Maps format strings to their Nav types:
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;
}Data Sources
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 Support
| Runtime | Version | Status | |---------|---------|--------| | Node.js | 22+ | Supported | | Bun | 1.0+ | Supported |
License
MIT
Contributing
Contributions are welcome. Please open an issue or submit a pull request on GitHub.
