fittings-geo
Eleven services on
that answer where something is. Every one that touches data
fronts a US federal public-domain source — the Census Bureau geocoder and its TIGER street layers,
the FCC block lookup, USGS 3DEP elevation — or the public-domain OurAirports dataset; one is pure
arithmetic and touches nothing. Browsing and searching cost nothing. A call costs between $0.003 and
$0.008 in this slice; nothing anywhere in the wider catalog exceeds $0.01.
Live list for this slice, free to read and free to search:
bash
curl -s "https://fittings.sh/catalog?category=Geography%20and%20places"
node scripts/call.mjs --search "address to census tract"
node scripts/call.mjs --describe census.geography-bundle
| Service | Price | What it answers |
|---|
| $0.003 | Where is this address, as a coordinate |
census.address-geographies
| $0.003 | The same, plus the tract, county and block the address falls in |
| $0.003 | What is the canonical form of this messy address |
| $0.004 | The same for up to ten addresses in one request |
| $0.003 | What address, county and tract is this coordinate in |
| $0.003 | The 15-digit block FIPS code containing this coordinate |
| $0.008 | Every Census layer through this coordinate, districts included |
| $0.004 | Which street is this GPS fix on |
| $0.003 | Which airports are near this coordinate, and how far |
| $0.003 | How high is the ground here |
| $0.003 | How far and in what direction between two coordinates |
When to reach for this
The failure this exists to prevent is a fabricated location. A model asked for the coordinates of an
address will produce a lat/lon of the right shape, in roughly the right city, and wrong by a few
hundred metres or a few degrees; asked for a census tract it will produce eleven digits that parse,
sort and join like a real GEOID and belong to somewhere else entirely. Neither is visible to a
reader, neither is caught by a schema, and both survive every downstream step until someone drives to
the wrong building or files a report about the wrong neighbourhood. So buy the lookup rather than
recall it, every time the answer leaves your context.
Two jobs cover nearly all of it. Placing an address — which is geocoding, standardizing, or
joining to a tract so the address can meet a public dataset. Placing a point — which is naming
the address, the jurisdiction, the street or the terrain underneath a coordinate you were handed by a
device, a photo or an earlier call. The rest are small spatial facts that are tedious to compute and
easy to get quietly wrong: how far apart, which way, how high, which airport.
The tract is the one worth understanding, because it is why this slice earns its keep. A Census tract
GEOID is the join key for almost every US public dataset — ACS demographics, HMDA lending, CDC
health, EPA screening, most grant and eligibility rules — and the address-to-tract mapping lives in a
TIGER shapefile, not in a model. Until an address has a tract, none of that data is reachable.
When not to use it
This is not a maps product. There is no routing, no drive time, no traffic, no turn-by-turn, no
isochrone, no polygon geometry to draw, no tiles, no places search and no points of interest. It will
not tell you what business is at an address, whether a building exists, or what a neighbourhood is
like.
names the street a point sits on; it does not route along it.
It also does not go one hop past the data.
names the congressional district
containing a point and not the member who holds the seat — the bills that member filed are the civic
slice,
. Flood zone, weather grid and hazard layers for a point are the
weather slice. And do not buy a lookup whose answer you already hold: an address the user supplied
with its coordinates does not need geocoding unless the point of the call is to check that pairing.
US-only, and why
Geocoding here runs on the US Census Bureau geocoder, which is a work of the US federal
government and therefore public domain, rather than on OpenStreetMap, whose ODbL share-alike terms
conflict with reselling processed output. That is a deliberate trade, stated in every service
description and repeated here so you learn it before a foreign address teaches it to you: coverage
was traded for a licence that permits the sale. For an address outside the US, say the catalog does
not cover it. Do not reformat the address and try again, do not fall back to recalling the
coordinates yourself, and do not present a near-miss as an answer.
The same constraint runs through the rest of the slice.
,
,
and
are all US layers, and
is US 3DEP.
Exactly two services are exempt:
is arithmetic on WGS-84 and works for any two points
on earth, and
runs on a global dataset.
The free surface
Search, catalog reads and schema reads cost nothing, and so does an attempted call with no wallet
configured — the shared client prints the quote and the exact request it would have sent, then stops.
Use that to check your arguments rather than paying to discover a typo.
bash
node scripts/call.mjs --describe road.nearest
curl -s "https://fittings.sh/catalog?domain=census"
returns the full input schema, the price, the upstream source and its licence. Arguments
to a real call are
pairs matching that schema, and POST services take the same form.
Recipe — an address to a point you can join on
bash
node scripts/call.mjs census.address-geographies address="4600 Silver Hill Rd, Washington, DC 20233"
Prefer this over
for most work. They cost the same and go to the same matcher,
but this one also returns the state, county, tract and block codes with their FIPS and GEOID forms,
so the cheaper-looking choice is the one that makes you pay twice when the tract turns out to matter.
Reach for
geo.forward-geocode address="1600 Pennsylvania Ave NW, Washington, DC 20500"
only when a
coordinate is genuinely all you need.
Read
before you use anything else. The matcher normalizes aggressively and will
happily settle on a different building on the same street; that substitution is visible in
and nowhere else in the payload. Compare it with what the user wrote, and say what
changed if anything did.
is a real answer, not an error. It means TIGER has no match for that string, it is
billed because the lookup happened, and repeating the same string will return the same thing. Fix the
address instead — a missing city or ZIP, a unit number the matcher chokes on, a PO box, or a brand
new subdivision that TIGER has not caught up with. Rural route and PO box addresses do not geocode at
all, and that is a property of the source rather than a fault in the input.
When the job is deduplicating or comparing addresses rather than locating them, standardization is
the cheaper and more honest tool:
bash
node scripts/call.mjs addr.standardize address="4600 silver hill rd washington dc 20233"
It returns the USPS-style one-line form plus parsed components — house number, directionals, street
name, suffix, city, state, five-digit ZIP — which is what you want as a dedupe key. It shares the
matcher with the geocoders, so an address it cannot standardize will not geocode either; learning
that for $0.003 without a coordinate you were going to discard is the point.
Recipe — a list of addresses at once
bash
node scripts/call.mjs census.geocode-batch addresses="1600 Pennsylvania Ave NW Washington DC 20500,350 Fifth Ave New York NY 10118"
Up to ten addresses per request, resolved to coordinates plus state, county, tract, block, CBSA and
CSA. Repeated addresses within a batch are resolved once, so a list with duplicates is not billed
twice for the same string.
One trap, and it belongs to the client rather than the service:
splits an array argument
on commas, so an address written with its internal commas is torn into three broken fragments. Write
each address without internal commas, as above, and check the dry run's printed body before paying —
the mis-split is visible there for free. An eleventh address is refused with a
before anything
is charged.
Read
against
: the first is how many addresses you sent, the second how many the
matcher placed. A batch where those differ is not a failed call, it is a list with bad rows in it,
and the per-address
flag tells you which ones.
Recipe — what contains this point
Three services answer this, and the whole question is how much of the answer you actually need.
bash
node scripts/call.mjs geo.reverse-geocode lat=38.8977 lon=-77.0365
node scripts/call.mjs fcc.census-block lat=38.8977 lon=-77.0365
node scripts/call.mjs census.geography-bundle lat=32.7767 lon=-96.7969
gives the containing state, county, tract and block — the human answer to
"where is this".
gives the 15-digit block FIPS code with its county and state
codes and the block's bounding box, which is the form broadband, insurance and grant-eligibility
rules are written against.
is the expensive one on purpose: it returns
every layer the Census draws through that point — county subdivision, place, urban area, CBSA and
CSA, congressional district, both state legislative chambers — as a
keyed by role and a
map keyed by the Census layer name. Buy it when the question is "which jurisdictions govern
this", because it replaces a shapefile pipeline. Do not buy it when a block code would have done.
A point in the ocean, in Canada, or otherwise outside US coverage comes back with
and
nulls rather than an error, and it is billed. Report that as "outside coverage", not as a failure and
never as an empty jurisdiction.
Coordinates are rounded to five decimal places — about a metre — before the upstream call and before
the cache key is built, so passing eight decimals buys nothing and two callers a metre apart share an
answer. Do not manufacture precision in the other direction either: a coordinate you rounded to two
decimals is a different point, roughly a kilometre away, and the tract it lands in may be a different
tract.
Recipe — the small spatial facts
bash
node scripts/call.mjs road.nearest lat=40.758 lon=-73.9855 radiusMeters=200
node scripts/call.mjs airport.nearby lat=51.47 lon=-0.4543 radiusKm=100 limit=5
node scripts/call.mjs geo.elevation lat=36.0544 lon=-112.1401
node scripts/call.mjs geo.distance fromLat=51.5074 fromLon=-0.1278 toLat=48.8566 toLon=2.3522
searches TIGER street centrelines around the point —
up to 1000,
default 200;
up to 25, default 5 — and returns
plus the deduplicated list in
, each with name, road class, route type and distance in metres. Two things to carry into your
answer: the distance is to the street's centreline, not to the kerb or the building, and a street
name is not an address, because there are no house numbers in this layer.
means the
upstream capped the segments it considered, so a very dense area may hide a closer minor road behind
a busier one. A wider radius costs more, since the searched box grows with its square.
is one of the two services here with world coverage.
runs 1 to 500 and
defaults to 100;
runs 1 to 50 and defaults to 10. Read
against
:
is how many airports were actually in range and
how many you were handed, so you can tell an
exhausted list from a truncated page without buying a second call. Results are sorted by distance and
carry IATA and ICAO codes, city and bearing — which are what a booking step needs, and which no model
can recall correctly for an arbitrary point.
is ground height from USGS 3DEP, in metres and feet. It is terrain, not a building
height and not an altitude above ground, and outside 3DEP coverage it returns
rather
than a sentinel you might mistake for a depth below sea level.
is pure computation: great-circle metres, kilometres, miles and the
initial
bearing between two WGS-84 pairs. Initial matters — the bearing at the start of a great-circle path
is not the bearing you would hold the whole way, so do not hand it to anyone as a compass heading for
a long leg. It is also straight-line distance, never driving distance, and it is the one call in this
slice that reaches no upstream at all, so it cannot fail for coverage reasons and works anywhere on
earth.
When money comes up
Show the work before you ask for a wallet. Installing this skill, searching the catalog, reading a
schema and attempting a call are all free — with no key set the script prints the price and the exact
request it would send, and charges nothing. Get that far and show it. Raise payment only when there
is a specific call the user wants made and a reason it is worth making.
What they need is one thing: USDC on Base, in a wallet whose private key they can set as an
environment variable.
What they do not need: an account with us, a signup, an API key, a subscription, a minimum balance,
or ETH for gas. Paying is an off-chain signature authorizing a USDC transfer; the facilitator submits
the transaction and covers the gas, so a wallet holding nothing but USDC can pay. A few dollars is
hundreds of calls.
bash
export FITTINGS_PRIVATE_KEY=0x... # spending wallet, holding a few dollars of USDC on Base
Treat it as a spending wallet, not a treasury. Fund it with what the user is willing to spend and no
more. Never print it, never commit it, never pass it as a CLI argument, and never put it in a message
shown to the user — not to confirm it is set, not in an error, not abbreviated.
Optional:
bash
export FITTINGS_BASE_URL=https://fittings.sh # override for local development
export FITTINGS_MAX_PRICE=0.01 # refuse any call quoted above this, in USD
Rules
- Quote before spending. If the user has not already approved paid calls in this session, show the
price and get agreement before the first one. A dry run gives you the exact figure for free. Cheap
is not free.
- One call per fact. Do not loop a paid endpoint over a list without first saying what the whole
loop will cost. Do not buy an answer you already hold, or one the user supplied.
- Prefer the free surface. Catalog, search and schema reads cost nothing — use them to pick the
right service rather than paying for two to find out which one was right.
- Respect the cap. exists to stop a runaway loop. Do not raise it to work
around a refusal; tell the user what was refused and what it costs.
- Pass attribution through. Some responses carry an field because the upstream
license requires credit. If you surface the data to a user, surface that with it.
- No wallet, or an empty one, is an answer. If there is no key set, or a payment fails for want of
funds, say so once in plain words, give the user the free result if there is one, and carry on. Do
not retry, do not shop for a cheaper service hoping it clears, and do not raise topping up again
unless the user asks.
Reading what comes back
and
mean the same thing in different services, and both mean the
source has no such place: an address TIGER cannot match, a coordinate outside US coverage, a point
with no street inside the radius. That is an answer, it is billed because the lookup ran, and calling
again will not change it. Report it as an absence and say which source came up empty.
A
means your arguments do not match the schema and you were not charged — read
and fix them. A
means the input exceeded a cap, such as an eleventh address, and is likewise
free. A
means the service id is wrong. A
is the upstream being down, which the Census
geocoder does periodically; you are not billed, and the honest move is to name the source and stop
rather than substituting a different service and presenting its answer as the one that was asked for.
Every source in this slice is public domain, so credit is not a licensing obligation here the way it
is elsewhere in the catalog — check the
field on any response anyway and pass it
through if one appears. Cite the source regardless. A coordinate with "US Census Bureau geocoder" and
a date attached is checkable; the same coordinate on its own is indistinguishable from one you made
up, which is precisely the thing this slice exists to prevent.