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

us-zip-centroids

v1.0.1

Published

Offline US ZIP code to latitude/longitude. Bundled Census ZCTA Gazetteer, no geocoder, no API key, no network call — and the address never leaves your infrastructure.

Readme

us-zip-centroids

npm CI dependencies node license

Offline US ZIP code → latitude/longitude. No geocoder, no API key, no network call.

npm install us-zip-centroids
const { zipToCoords, geocodePostalCode } = require('us-zip-centroids');

zipToCoords('30305');                                  // → [33.8341, -84.3921]
zipToCoords('abcde');                                  // → null
zipToCoords('2024-01-15');                             // → null, not a point in DC

geocodePostalCode('30305-1234', { country: 'USA' });
// → { latitude: 33.8341, longitude: -84.3921, source: 'zip_centroid' }

TypeScript types included. Zero dependencies.

Why not just call a geocoding API

Three reasons, and the third is usually the real one.

Cost. Commercial geocoders bill per request. Ranking search results by distance means resolving a coordinate on a hot path, and that adds up fast for something that never changes — a ZIP's centroid is the same today as it was last year.

Latency and failure. A network call in the middle of a query is a dependency that can be slow, rate-limited, or down. This is a memoized in-process lookup after the first read.

Privacy. Sending user addresses to a third party is a data-sharing decision, not just a technical one. Depending on what you're building, it may be one you have to disclose or justify. This resolves entirely inside your own infrastructure.

What you get, and what you don't

A ZIP centroid is the middle of an area, not a building. Precision is about 11 m at four decimal places, which is ample for:

  • bounding-box filtering and haversine distance ranking
  • "within N miles of me" search
  • coarse regional grouping and analytics
  • resolving a spoken or typed ZIP to a search center

It is not a substitute for street-level geocoding. If you need to put a pin on a specific address, use a real geocoder.

API

zipToCoords(zip)[lat, lng] or null. A fresh array each call, so mutating it can't corrupt the shared table.

geocodePostalCode(code, opts){ latitude, longitude, source } or null. opts.country accepts ISO-3166 alpha-2, alpha-3 and numeric ('US', 'USA', 840), case- and whitespace-insensitive; anything else returns null rather than a guess.

normalizeZip(zip) → canonical 5-digit string or null. See below for exactly what it accepts.

isValidZip(zip) → boolean. True for a well-formed ZIP whether or not it's covered, so you can tell "that wasn't a ZIP" from "we don't have that one".

datasetInfo(){ path, isOverride, entries, error }. Assert this at startup — a dataset that failed to load is otherwise only observable as an endless run of nulls, which reads exactly like "we don't cover those ZIPs":

const { entries, error } = datasetInfo();
if (error) throw new Error(`ZIP centroids unusable: ${error}`);

clearCache() → re-reads the file on the next lookup. ZIP_CENTROIDS_PATH is read once, on first use.

Behaviour worth knowing

It fails loud, never guesses — and that's mostly about parsing. Any string with five digits in it could be read as a ZIP, and a naive reading turns '2024-01-15' into 20240 and 'Suite 200, 123 Main St' into 20012 — both real coordinates in Washington DC. A wrong point is worse than no point, because downstream it's indistinguishable from a real one.

So normalizeZip accepts exactly three shapes:

| Input | Result | | |---|---|---| | '30305', '30305-1234', '303051234' | '30305' | the whole value is a ZIP | | '3 0 3 0 5' | '30305' | digits and separators only, at 5 or 9 digits | | 'Atlanta, GA 30305' | '30305' | a delimited 5-digit run in prose | | '2024-01-15', '404-555-1234' | null | 8 and 10 digits — not truncated to fit | | 'Suite 200, 123 Main St' | null | no 5-digit run |

US-only, deliberately. A non-US postal code is a coverage gap, not an answer, and returns null.

Leading zeros are restored on numbers. 1001 and 1001.0 normalise to '01001', because a number cannot carry a leading zero — recovering it is lossless, not a guess. A string '1001' is null: it's as likely to be a typo as a mangled ZIP. Northeastern ZIPs lose their zero the moment anything treats them as a number, and this is the single most common source of "why does Massachusetts geocode to nowhere".

An unreadable data file degrades to empty rather than throwing — every lookup returns null, the failure is reported on stderr with its error code, and datasetInfo().error carries it for a healthcheck. That covers a missing file, a path pointing at a directory, and a permissions problem alike. A geocoding table that vanished should not take your process down, but it also shouldn't be invisible.

A CSV that parses to zero rows warns too. A file truncated by a bad build looks exactly like a working install from the outside.

Coordinates are bounds-checked. Since ZIP_CENTROIDS_PATH lets you supply the table, a row with an impossible latitude is dropped rather than trusted.

The data

data/zip_centroids.csv is the full US Census ZCTA Gazetteer — roughly 33,000 ZCTAs with national coverage including Alaska, Hawaii and Puerto Rico. It is generated from the authoritative Census file, never hand-authored, and is a US Government work in the public domain.

Point ZIP_CENTROIDS_PATH at another CSV (zip,lat,lng) to pin a fresher vintage or a custom table at deploy time without a code change.

The table is parsed once per process and memoized.

Testing

npm test

Node's built-in test runner. No dependencies.

CI also packs the tarball, installs it into a clean project, and geocodes from it. The data file is the product here, so a files entry that stopped matching data/ would publish a package whose every lookup returns null — and every test would still pass, because they run against the source tree.

Related packages

Small, dependency-light pieces pulled out of production systems I've built:

License

MIT © Drew Thomas for the code. The bundled Census data is a US Government work in the public domain.