https://austinhenley.com/blog/coord2state.html
Austin Z. Henley
Associate Teaching Professor
Carnegie Mellon University
azhenley@cmu.edu
@austinzhenley
github/AZHenley
---------------------------------------------------------------------
Home | Publications | Teaching | Blog
---------------------------------------------------------------------
Mapping latitude and longitude to country, state, or city
6/3/2025
[stateborde]
See the discussion of this post on Hacker News.
An app can easily check a user's location (with permission), but
figuring out where that location is, is far more difficult. For
example, a web app can use geolocation.getCurrentPosition() to get a
user's location in the form of latitude and longitude coordinates.
But how do you convert coordinates to country, state, or city? Well,
you have to look it up, somehow. (I later learned this is called
reverse geocoding.)
At the startup I worked at, we paid several thousands of dollars per
year for the Google Maps API to do a reverse lookup (that decision
was made long before I joined). Given the coordinates, it gave us
back a full address. We only cared about the state they were in, so
we ignored the rest. If the user didn't allow us to check their
location, we simply showed a state selection screen:
[stateselec]
We paid thousands of dollars a year, just for users to avoid this
screen!? Yep.
Shortly after I joined the company, I tried to find a library that
lets you lookup the state from coordinates, but I didn't find one. It
seemed like the sort of thing that should exist. There are a lot of
products that want to show you the nearest store. Some products use
the user's IP address to determine the location. Or we could have
done a simple distance check to a set of known points. Either would
have been good enough.
Now that I don't have a need for such a library, I went down the
rabbit hole and made coord2state based on actual US state borders for
quick lookups. It is a single-file JavaScript library with no
dependencies that can run client-side to determine which state a
given latitude and longitude point falls in. It is 260 KB and is
99.9% accurate compared to the US Census Bureau's borderlines. You
could have 100% accuracy if you were ok with 21 MB! It is on GitHub
and NPM.
Try it:
Lat: [42.9 ] Lon: [-75.4 ]
Lookup
State code: ?
---------------------------------------------------------------------
How does it work?
The US Census Bureau publishes border data for use in GIS software. I
downloaded the state borders from 2024, which came in a 50 MB binary
file detailing the vectors of each border. Luckily there are several
Python libraries that make these files easy to work with.
states = gpd.read_file("tl_2024_us_state.shp")
lon, lat = -74.0060, 40.7128 # New York
match = states[states.geometry.contains(Point(lon, lat))].iloc[0]["NAME"]
I could just wrap this up in my own web API and avoid the expensive
Google Maps API. It would work well, but that is boring. My goal is
to do it all client-side.
At a first glance, the borders are really detailed.
State Vertices
Texas 62855
Virginia 49707
Minnesota 40293
West Virginia 35242
Idaho 35120
Kentucky 34623
North Dakota 32441
North Carolina 32133
Georgia 30758
Arkansas 29782
Tennessee 28874
Montana 26724
Maryland 26093
Mississippi 25719
Iowa 24200
Louisiana 21547
Oklahoma 21192
Florida 20603
Missouri 20283
South Dakota 19954
Illinois 19761
Maine 18932
South Carolina 17509
Ohio 17354
New Hampshire 16608
Indiana 16241
Wisconsin 16184
Pennsylvania 15994
Alaska 15329
Oregon 14581
Nebraska 13992
Alabama 13638
Kansas 13285
Vermont 12119
California 12103
Arizona 10816
Michigan 10705
New York 10276
Nevada 9537
Wyoming 8591
Washington 7945
New Jersey 7896
Colorado 7513
Utah 7490
District of Columbia 7365
New Mexico 7097
Delaware 5345
Massachusetts 4783
Hawaii 3519
Connecticut 3204
Rhode Island 2669
Thousands and thousands of vertices for a single state seems like way
more precision than we need. For example, North Dakota has 32,441
vertices despite it being a rectangle with three straight sides and
one jagged side that follows a river.
[bordersnor]
Surely we could simplify the borders a bit without losing much
accuracy? The Shapely library already implements the Douglas-Peucker
algorithm for simplifying lines. It removes points that don't change
the overall shape within a specified tolerance.
With a tolerance of 0.5deg (not an angle, but a linear distance
measured in degrees of latitude/longitude), it reduces the number of
vertices by 99.9% for most states. For example, Texas is reduced from
62,855 vertices to 14!
[borderstex]
You can see that it oversimplifies by quite a bit though. It takes
going down to 0.01deg tolerance to make the differences indiscernible
at this resolution, and yet it still reduces the vertices for most
states by 99%. Texas is reduced by 98.8% to 756 vertices:
[borderstex]
How much accuracy are we losing though? In terms of total area, it is
a very small amount of error. Far less than 1%. I set up an
experiment that compares the original geometry with the simplified
geometry by testing 1,000,000 random points within the US.
[bordersnot]
A tolerance of 0.1deg gives an accuracy of 99.3%!
Alright, that seems like it is probably accurate enough for most use
cases. But none of this matters unless we can get the size down. I'd
really like it to be less than 1 MB if this is going to run client
side.
I need to convert the geometry data into an easier to use JSON format
along with some JavaScript code that can look up which polygon a
given point is in. Again, we can use an existing library to do all
the hard work for us. Geopandas can load the geometry data and
convert it to JSON. ChatGPT wrote a Python script that injects the
JSON into this JavaScript template (it does find and replace on
"geojson" with the JSON literal):
[bordersjst]
Ok, so now I have a working JS library that I could potentially ship.
Next, I need to measure how much space it takes! A tolerance of 0.1deg
results in a 168 KB size JS file. We can minify it though. That
brings it down to 55 KB.
That is definitely usable as a client-side library!!!
I wrapped it all up into a Jupyter notebook that compares accuracy
and file size based on tolerance so that I could find the ideal size
to go with. The results:
Tolerance Accuracy Size Minified
0.000deg (baseline) 100.0000% 72 MB 21 MB
0.001deg 99.9909% 4.2 MB 1.2 MB
0.005deg 99.9505% 1.5 MB 439 KB
0.010deg 99.9067% 870 KB 260 KB
0.050deg 99.6473% 257 KB 81 KB
0.100deg 99.3468% 168 KB 55 KB
0.500deg 96.3959% 96 KB 34 KB
1.000deg 92.3244% 89 KB 33 KB
These results show that a small drop in accuracy gives a huge drop in
file size. Based on this, a tolerance of 0.01deg looks like a nice
balance.
Can it be improved?
It is worth pointing out that my evaluation of accuracy is limited.
The benchmark randomly chooses points within the bounding box of the
continental US, but the population of the US does not follow a random
distribution. The test could be expanded to generate points based on
a population density map.
Also, testing points that are obviously in the center of a state is
not interesting. It is near borders that we actually care about. The
overall results will hide areas with high error rates, such as
islands or narrow borders. The next step would be to identify these
areas and build a test set for them.
I casually scanned the 10 most populous cities to see if they were
being impacted by the simplfied borders. At 0.1deg, there are some
issues, such as a sliver of Staten Island being considered New
Jersey. Don't come after me, please. At 0.01deg, the problem goes
away.
It could improve results to use a different precision for states
based on their population or density.
A side effect of the geometry simplication is that there are some
very small gaps between states. Based on your use case, you'll need
to handle the case of the point not being within any state borders.
In these rare cases, you could fall back to a different method, such
as distance checking centroid points, adding an episilon to all state
borders, or simply asking the user. (The user may also be in another
country or in the ocean...)
It is also possible to apply my exact approach to any other borders,
such as the city borders provided by the US Census Bureau. However,
this is going to require significant memory, so you wouldn't want to
ship it as a client-side JS library. You'll want a more efficient
data structure for doing the lookup too (e.g., binary space
partioning).
---------------------------------------------------------------------
For most casual use cases, the library will be fine as is. I packaged
it with a tolerance of 0.01deg which I believe gives plenty of accuracy
and minifies to 260 KB. You can find it on GitHub and NPM.
npm install coord2state