A supervisor sends over a CSV. One column. 150 Swedish cities. One question: "Är denna ort inom 60 km från Strängnäs?" — is this city within 60 km of Strängnäs?
Doing this by hand would mean looking up 150 cities on a map, eyeballing distances, and hoping you don't mix up Hjo and Hörby. Instead, we wrote a script that geocodes every city and calculates straight-line distances using the same geographical tooling already in use on a client project.
Reusing What Already Exists
One of the rental property platforms we maintain uses OpenStreetMap APIs for geographical features — nearby amenities, map rendering, address search. The shared utilities package contains a Haversine formula for calculating distances between two geographic coordinates, and the platform queries the Overpass API to find points of interest within a radius.
For this task, we needed two things from the OSM ecosystem:
The client project also uses MapTiler for interactive maps and address geocoding on the frontend. MapTiler requires an API key and is designed for user-facing map interactions — overkill for a batch script that just needs coordinates from city names. Nominatim was the right choice here: free, keyless, and purpose-built for geocoding.
The Haversine Formula
The client project's calculateDistance function implements the Haversine formula, which computes the shortest distance between two points on a sphere given their latitudes and longitudes. The Earth isn't a perfect sphere, but for distances of tens of kilometers in Sweden, the approximation is accurate enough.
The client project's version includes a 20% buffer and rounds up to the nearest 100 meters — appropriate for estimating walking or driving distances to nearby amenities. For our use case, we stripped that out. We needed the raw great-circle distance to answer a binary question: within 60 km or not.
The client code:
const distance = R * c;
return Math.ceil(distance / 100) * 100 * 1.2; // Rounds up, adds 20% bufferOur adapted version:
const R = 6371; // Earth's radius in kilometers (not meters)
const distance = R * c;
return distance; // Raw distance in km — no buffer, no roundingThe only other change was the Earth's radius constant. The client uses 6371e3 (meters) since it works with walking distances to cafes and bus stops. We used 6371 (kilometers) since we're measuring city-to-city distances.
Nominatim Geocoding
Nominatim is OpenStreetMap's geocoding service. Send it a place name, get back coordinates. The API is simple — a GET request with query parameters.
Two important constraints with Nominatim. First, you must include a User-Agent header identifying your application — requests without one get rejected. Second, the rate limit is strict: one request per second. For 150 cities, that means the script runs for about two and a half minutes. No way around it without setting up a self-hosted Nominatim instance, which would be absurd for a one-off task.
We appended ", Sweden" to every city name in the query to avoid ambiguity. A bare "Lund" could return Lund in Norway or Lund in the UK. Adding the country context ensures Nominatim returns the Swedish city.
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
city + ", Sweden"
)}&format=json&limit=1`;
const response = await fetch(url, {
headers: { "User-Agent": "CityDistanceChecker/1.0" },
});The limit=1 parameter tells Nominatim to return only the top result. For well-known cities, the first result is almost always correct. For ambiguous names, it returns the most prominent match — which for Swedish cities in a Swedish context is the right behavior.
The Script Pipeline
The complete flow reads the CSV, geocodes Strängnäs as the reference point, then iterates through every city with a one-second delay between requests.
The CSV had one surprise: an empty line at row 32. The script filters empty lines after splitting, so it processed 149 cities rather than 150. Strängnäs itself appears in the list and correctly returns 0.0 km — a useful sanity check that the geocoding and distance calculation pipeline works end to end.
Adapting the Distance Formula
The adaptation from the client project was minimal but worth calling out. The original function signature uses lat and lng for parameter names. Nominatim returns lat and lon. A trivial difference, but the kind of thing that causes bugs if you're blindly copying function calls.
// Client project: calculateDistance(lat1, lng1, lat2, lng2)
// Nominatim returns: { lat: number, lon: number }
const distance = calculateDistance(
strangnas.lat,
strangnas.lon, // "lon" from Nominatim maps to "lng1" parameter
coords.lat,
coords.lon // Same mapping
);The underlying math doesn't care whether you call it lng or lon — it's the same number. But when reading the code six months later, the inconsistency in naming could raise questions about whether the right values are being passed. In a production system, you'd normalize the naming. For a one-off script, it's fine.
Results
Of 149 cities, 21 fell within the 60 km radius. The closest was Strängnäs itself at 0 km. The farthest within range was Täby at 59.6 km — barely making the cut. The nearest miss was Köping at 60.2 km.
The geographic pattern makes intuitive sense. The "Ja" cities form a cluster around the Mälardalen region — Stockholm suburbs, the Västerås-Eskilstuna corridor, and towns along Lake Mälaren. Everything south of Nyköping, west of Örebro, or north of Uppsala falls outside the radius.
Straight-Line vs. Driving Distance
One caveat worth noting: the Haversine formula calculates great-circle distance — the shortest path along the Earth's surface. Actual driving distance is always longer due to roads, lakes, and terrain. A city at 58 km straight-line might be 75 km by road.
The supervisor's question used "60 km" without specifying straight-line or driving. We went with straight-line, which is the more conservative interpretation — if a city is more than 60 km in a straight line, it's definitely more than 60 km by road. Cities right at the boundary (Köping at 60.2 km, Stockholm at 59.5 km) could go either way depending on interpretation.
If driving distance precision were needed, the OpenStreetMap ecosystem has OSRM (Open Source Routing Machine) for that. But for a yes/no classification with a clean margin, Haversine is sufficient.
Takeaways
First, reuse utilities from existing projects. The Haversine formula was already written, tested, and understood. Adapting it took two minutes. Writing and validating it from scratch would have been unnecessary work.
Second, Nominatim is the right tool for batch geocoding simple place names. It's free, accurate for well-known cities, and the API is trivial. The only cost is the rate limit — but for a one-off script, waiting two minutes is a non-issue.
Third, always add country context when geocoding. Bare city names are ambiguous. "Lund" exists in multiple countries. Appending ", Sweden" eliminates that ambiguity at the query level.
Fourth, straight-line distance is a conservative lower bound. If a city fails the straight-line check, it will definitely fail a driving distance check. Cities right at the boundary warrant a follow-up with a routing API if precision matters.
Fifth, include the reference point in your dataset as a sanity check. Strängnäs appearing in the CSV and returning 0.0 km confirmed the entire pipeline — geocoding, coordinate handling, and distance calculation — was working correctly end to end.