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

@lovett/geo

v0.4.2

Published

Typed client for the geocoder engine (geocode.edwinlovett.com) — the family's geometry oracle. Types and runtime shapes are generated from the engine's own OpenAPI spec, so a new endpoint reaches consumers by bumping this package, not by hand-writing a fe

Readme

@lovett/geo

The typed client for the geocoder engine — the family's geometry oracle at geocode.edwinlovett.com.

Its types are generated from the engine's own openapi.json — all of it. There are no hand-written request/response types in this package. That is the whole point: an endpoint the engine gains reaches consumers by bumping this package, not by someone hand-writing another fetch and getting the shape subtly wrong.

Zero runtime dependencies — fetch, URL, AbortController only. Runs unchanged in Cloudflare Workers, browsers, and Node ≥ 18.

pnpm add @lovett/geo

Use it

import { createGeoClient, isMiss, isGeocodeHit } from '@lovett/geo'

const geo = createGeoClient()            // defaults to geocode.edwinlovett.com

// Geo toolkit
const fit   = await geo.geo.fitRadius({ zips: ['75034', '75035', '75024'] })
const reach = await geo.geo.reach({ zips: fit.zips })          // aggregate totals, one call
const ring  = await geo.geo.ring({ center: { zip: '78701' }, inner_miles: 2, outer_miles: 8 })

// ZIP reference
const page  = await geo.zips.search({ query: 'Frisco, TX' })
const near  = await geo.zips.neighbors('75034')

// Geocoding
const hit = await geo.geocode('3015 W Kennedy Blvd, Tampa, FL')
if (isGeocodeHit(hit)) console.log(hit.lat, hit.lng)

// Batch geocode 23k rows: submit, poll, collect — one call
const out = await geo.batchToCompletion(addresses, {
  onProgress: (s) => console.log(`${s.percentage}%`),
})
out.results // one JobRow per address

// Which provider is right? Ask all of them.
const audit = await geo.geocodeAll('3015 W Kennedy Blvd, Tampa, FL')

Demographics without paying for demographics

The engine deliberately keeps ACS out of the geometry responses. Population is opt-in, in three shapes, and picking the right one is the difference between one request and four hundred:

// Aggregate — the reach counter. One call, any size selection.
await geo.geo.reach({ zips })                       // → { population, households, unit_count }

// One point — the hover tooltip. Full ACS profile.
await geo.geo.demographics({ zip: '78701' })        // → median income, age, tenure, …

// Many ZIPs — the choropleth / table column. One call, and you pick the columns.
await geo.geo.demographicsBatch({ zips, fields: ['population'] })
// → results: [{ zip: '78701', population: 10659 }, …]   ← nothing you didn't ask for

A miss is a value, not an exception

The engine answers a lookup miss with a body, not a throw — and this client keeps it that way. Branch, don't try:

const zip = await geo.zips.get('99999')
if (isMiss(zip)) return null                        // zip.error is a string here
zip.zipcode?.city                                   // safe

const hit = await geo.geocode(row.address)          // one bad row in 23k is routine
if (!isGeocodeHit(hit)) markForReview(row)         // a miss is a 404, folded to a value

Real failures still throw a GeoApiError — branch on .code (INVALID_PARAMS · OUT_OF_RANGE · NOT_FOUND · INTERNAL · BACKEND_UNAVAILABLE), not on .status. The enum is append-only, so an unknown code is passed through rather than rejected. Timeouts throw GeoTimeoutError.

Options

createGeoClient({
  baseUrl: 'http://localhost:8080',   // point at a local `cargo run`
  timeoutMs: 30_000,                  // default 15s; 0 disables
  headers: { authorization: '…' },    // for when the engine takes a service token
  fetch: myFetch,                     // inject for tests / retries / tracing
})

Every call takes the same knobs per-call: { timeoutMs, signal, headers }.

Call geo.warmup() fire-and-forget on app load — the engine lazily builds its indexes, and this turns a ~150 ms first-request penalty into 0.

Picking up an engine change

The engine is the source of truth. This package is a projection of it.

pnpm sync      # pull the live openapi.json + regenerate src/generated/schema.ts
pnpm verify    # build + typecheck + test
# bump the version, publish, and consumers adopt it when they choose to

pnpm sync fetches from production. Use pnpm gen to regenerate from the checked-in openapi.json instead (offline, reproducible).

A new endpoint is usable the moment you regenerate — you do not have to wait for a facade method, because the transport is generic over the spec:

await geo.http.post('/api/v1/geo/brand-new-thing', { … })   // fully typed

The methods on geo.geo.* / geo.zips.* / geo.boundaries.* buy naming and grouping, nothing more. Their parameter and return types are derived from the spec by path, so neither a shape change nor a schema rename in the generator can silently drift them.

Consumers pin a version on purpose. A breaking engine change cannot quietly break eleven engines at once — each adopts it when it bumps.

The engine's spec covers 100% of its API

Every JSON route the engine serves is in openapi.json, so every type here is generated. The engine's own drift check (scripts/check-endpoints.mjs) fails the build if a route is added without documenting it, and its EXCLUDES list is down to three entries — the HTML root, the Scalar page, and the spec document itself, none of which serve JSON.

This was not free. Geocoding (/api/geocode, /api/batch, /api/job/*) used to be undocumented, which forced this package to carry a hand-typed legacy.ts — and that file was already wrong about two things before it was a day old (a geocode miss is a 404, not the 200 the docs implied; and the non-v1 routes answer with {success,error}, not the {code,message} envelope). Both were caught by the live suite, and both are now encoded in the spec. legacy.ts is deleted.

Two error shapes, and the spec now distinguishes them:

| Routes | Body | Branch on | |---|---|---| | /api/v1/* | ApiError{code, message, details?} | err.code (append-only enum) | | everything else | LegacyError{success:false, error} | err.code too — the client normalizes it |

GeoApiError papers over the difference: it derives a code from the status when the body has none, so switch (err.code) works against either shape.

Testing

pnpm test        # unit — mocked transport, no network
pnpm test:live   # smoke — hits the real engine (GEO_LIVE=1)

The live suite is the one that earns its keep: it is what caught the 404-vs-200 geocode miss above. A generated type only proves the spec is consistent; only a live call proves the engine is.