How to geocode a list of US cities in Python — no paid API
To geocode a list of US cities without a paid API, merge your cities against a reference List that already carries coordinates. Load both as pandas DataFrames, line up the join key — city name plus state, or the exact fips_place_code — and merge to attach lat and lng. No API calls, no rate limits, no per-request cost; the geocoding is a table join you run locally.
Get the free Basic City List.
Check the join keys and column names before you buy — no account needed.
Paid geocoding APIs meter every row and cap your throughput. When your input is US cities that already exist in Census data, you do not need a geocoder at all — you need the coordinates joined to your names.
What you have and what you join on
- Source shape: a CSV or DataFrame with a city column, ideally city plus state (
Springfield, ILis answerable;Springfieldalone is not). - Reference List: the City List with Coordinates — 48,490 rows with
name,state_name,fips_place_code,lat, andlng, among other columns. - Join key — pick one:
fips_place_code(7-digit place code) — exact and unique. Use it if your data already has it.name+state_name— use when all you have is text; needs light normalizing first.
Joining on fips_place_code is the reliable path: the place code is unique, so the merge cannot confuse two cities that share a name. Names need cleanup before they match.
Merge on the place code (the clean path)
If your source already carries a fips_place_code, the join is three lines:
import pandas as pd
mine = pd.read_csv("my_cities.csv") # has a fips_place_code column
ref = pd.read_csv("us-city-list_coordinates.csv") # the reference List
out = mine.merge(
ref[["fips_place_code", "lat", "lng"]],
on="fips_place_code",
how="left",
)
Every matched row now has lat and lng. Rows with no match come back as NaN — count them with out["lat"].isna().sum() so a silent miss never ships as a blank coordinate.
Merge on city + state (when all you have is names)
City names do not match cleanly across sources — case, punctuation, and the "city/town" suffix all differ. Normalize both sides to the same shape, then merge on the pair:
import pandas as pd
def norm(s):
return (
s.str.strip()
.str.lower()
.str.replace(r"\s+(city|town|village|borough|cdp)$", "", regex=True)
.str.replace(".", "", regex=False) # St. Louis -> st louis
)
mine = pd.read_csv("my_cities.csv") # columns: city, state (2-letter)
ref = pd.read_csv("us-city-list_coordinates.csv")
mine["k_city"] = norm(mine["city"])
ref["k_city"] = norm(ref["name"])
out = mine.merge(
ref[["k_city", "state_name", "lat", "lng"]],
left_on=["k_city", "state"],
right_on=["k_city", "state_name"],
how="left",
)
Always join on state as well as city. Without it, the merge treats every Springfield as the same place and attaches the wrong coordinates.
Duplicate names and the Springfield problem
Dozens of US cities share a name — there are Springfields in Illinois, Missouri, Massachusetts, and more. On a name-only join, pandas matches the first one it finds and your coordinates are wrong for the rest. Two fixes: add state_name to the join (above), or switch to fips_place_code, which is unique per place and removes the ambiguity entirely.
One more source of misses: the List includes CDPs (census-designated places) and townships, so a name may resolve to a populated place you did not expect. Inspect the unmatched and many-matched rows before you trust the output — out[out["lat"].isna()] and a groupby count on your key are enough to catch both.
The reference List
The City List with Coordinates ($29) is the file behind the merge: one UTF-8 CSV, 48,490 rows, with fips_place_code, name, state_name, county_name, lat, lng, and geography columns like elevation_ft, timezone, and cbsa_name. It loads straight into pandas with read_csv and downloads instantly after checkout.
Geocoding a handful of cities? The city geocoder tool handles single lookups, free. Building the pipeline first? Download the free 100-row sample and write your merge against real values before you buy the full List.
lat and lng are place-level coordinates in WGS 84, built for mapping and joins rather than street-level routing. Columns are compiled from U.S. Census Bureau, HUD, and Department of the Interior releases and refresh as those publish; where a List includes population, that figure is a 2024 estimate and Demographics-tier income and housing columns are 5-year averages — estimates, not exact counts.
Next: get the free Basic City List to check the join keys, or open the City List with Coordinates ($29) for the full file.