How to draw and view boundary data with OpenStreetMap OSM
For my last post in our OpenStreetMap (OSM) and OSM Nominatim API tutorial series, I'll show you how to use OSM to draw and view boundaries on a map.
Part 1: OpenStreetMap OSM Nominatim API tutorial
Part 2: Run a free geocoder with Nominatim Docker
Part 3: Building a free geocoding and reverse geocoding service with OpenStreetMap
Part 4: How to draw and view boundary data with OpenStreetMap OSM (this article)
In the first part of this blog post, I will explain how to use Nominatim's search and lookup endpoints to find and retrieve boundary data. In the second, I'll demonstrate how to build a simple web application that uses this data to draw boundaries on a map.
As with the other tutorials on this blog, full source code for this project, openstreetmap-boundaries, can be found on GitHub.
Why draw boundaries on a map?
Mapping out boundaries and territories on a map is beneficial for several reasons. For example, during the COVID-19 pandemic, the Centers for Disease Control and Prevention (CDC) in the United States managed a real-time heat map that displayed the rates and severity of COVID-19 infections, broken down by each county.
Field services e.g. utility companies, use boundary mapping to ensure that they are operating in the right areas while logistics companies often map out territories to prevent overlaps in service areas and ensure that each area is adequately serviced without redundancy.
How do I view boundary data on a map?
The easiest way to view boundary data is to open Google Maps and type in the name of the area you want see. For example, searching for "Vancouver" brings up the territorial boundaries for the city of Vancouver, outlined in red dots.
However, if you want to use this data in your own mapping application, you're out of luck. Google Maps does not make this information available via API or in their search results.
Retrieving Boundary Data from the OSM Nominatim API
Luckily, the OSM Nominatim API comes to the rescue. Recall from my last post that the search endpoint of the Nominatim API returns an array of objects, each representing a location. For example, the API call:
Method: GET
https://nominatim.openstreetmap.org/search?q=vancouver&format=jsonv2
returns three place
objects matching the search string "Vancouver":
[
{
"place_id": 330527268,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
"osm_type": "relation",
"osm_id": 1852574,
"lat": "49.2608724",
"lon": "-123.113952",
"category": "boundary",
"type": "administrative",
"place_rank": 16,
"importance": 0.6869679506485442,
"addresstype": "city",
"name": "Vancouver",
"display_name": "Vancouver, Metro Vancouver Regional District, British Columbia, Canada",
"boundingbox": [
"49.1989306",
"49.3161714",
"-123.2249611",
"-123.0232419"
]
},
{
"place_id": 329699655,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
"osm_type": "relation",
"osm_id": 2249770,
"lat": "49.592949450000006",
"lon": "-125.70255696124094",
"category": "place",
"type": "island",
"place_rank": 17,
"importance": 0.5462423036464551,
"addresstype": "island",
"name": "Vancouver Island",
"display_name": "Vancouver Island, British Columbia, Canada",
"boundingbox": [
"48.3088833",
"50.8768322",
"-128.4329230",
"-123.2647607"
]
},
{
"place_id": 284004308,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
"osm_type": "relation",
"osm_id": 237278,
"lat": "45.6306954",
"lon": "-122.6744557",
"category": "boundary",
"type": "administrative",
"place_rank": 16,
"importance": 0.5264689419276137,
"addresstype": "city",
"name": "Vancouver",
"display_name": "Vancouver, Clark County, Washington, United States",
"boundingbox": [
"45.5773887",
"45.6980040",
"-122.7745635",
"-122.4645600"
]
}
]
Two out of three of the returned items (osm_id
1852574 and 237278) are tagged with {"category": "boundary"}, indicating that these place
objects represent administrative boundaries, not specific addresses or locations.
OSM Nominatim API Lookup API
So how do we retrieve the boundary data associated with each location? The results returned above give us a boundingbox
object but this is not enough to draw boundaries on a map. We need an ordered array of coordinates that we can connect together from start to end. Here's where the Lookup endpoint comes in handy.
Endpoint GET
https://nominatim.openstreetmap.org/lookup?osm_ids={[N|W|R] + osm_id}&format={output_format}&polygon_geojson=1
{[N|W|R] + osm_id} is a comma-separated list of OSM ids each prefixed with its type, one of node (N), way (W) or relation (R). For example, to use the place
object with osm_id
1852574, you would have to first check its type (it's a "relation") and then append the prefix ("R") to the osm_id
like so: "R1852574".
{output_format} determines the structure of the response and must be one of xml
, json
, jsonv2
, geojson
, geocodejson
, the most common being jsonv2
.
polygon_geojson=1 forces Nominatim to return the coordinates of the place
object's boundary.
Here's a worked example of the Lookup API for osm_id
1852574 (Vancouver, Metro Vancouver Regional District, British Columbia, Canada):
Method: GET
https://nominatim.openstreetmap.org/lookup?osm_ids=R1852574&format=jsonv2&polygon_geojson=1
Output
[
{
"place_id": 330527268,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
"osm_type": "relation",
"osm_id": 1852574,
"lat": "49.2608724",
"lon": "-123.113952",
"category": "boundary",
"type": "administrative",
"place_rank": 16,
"importance": 0.6869679506485442,
"addresstype": "city",
"name": "Vancouver",
"display_name": "Vancouver, Metro Vancouver Regional District, British Columbia, Canada",
"address": {
"city": "Vancouver",
"county": "Metro Vancouver Regional District",
"state": "British Columbia",
"ISO3166-2-lvl4": "CA-BC",
"country": "Canada",
"country_code": "ca"
},
"boundingbox": [
"49.1989306",
"49.3161714",
"-123.2249611",
"-123.0232419"
],
"geojson": {
"type": "Polygon",
"coordinates": [
[
[
-123.2249611,
49.2746236
],
... // 126 entries
[
-123.2249611,
49.2746236
]
]
]
}
}
At last! We now have a geojson
object with the boundary coordinates:
"geojson": {
"type": "Polygon",
"coordinates": [
[
[
-123.2249611,
49.2746236
],
... // 126 entries
[
-123.2249611,
49.2746236
]
]
]
}
Viewing boundary data on a map
To check that the boundary is correct, copy and paste the geojson
object (above) into a new file (or download the gvrd-geojson.json file below), save it, and upload it to a geojson viewer such as geojson.io. Here's what the map boundary from the Vancouver example above looks like:
Building a boundary mapping tool with OpenStreetMap
Now that we know how to retrieve map boundary data from OSM Nominatim, the next step is to develop a web-based mapping tool to view the selected boundary (screenshot below).
openstreetmap-boundaries is a simple React app scaffolded with the Create React App build kit. It only has two main components - a search bar in the top left corner and a full screen interactive map layer.
To set up the project on your local machine, follow these steps:
- Clone the repository to your local system.
- Open your terminal, navigate to the project directory, and execute
npm install
to install the necessary dependencies. - After the installation is complete, run
npm start
to launch the application. - You need to create a
.env
file in the root of your project directory. Add the following line to this file:REACT_APP_MAPBOX_ACCESS_TOKEN={MAPBOX_ACCESS_TOKEN}
, replacing{MAPBOX_ACCESS_TOKEN}
with your actual Mapbox API key, which you can obtain from your Mapbox account. - This Mapbox API key is used to display map tiles in the application.
How our project is structured
I've designed openstreetmap-boundaries as a single page React app with one parent component (App.jsx
) containing two child components (CitySearch.jsx
and MapBox.jsx
). The CitySearch component is responsible for letting users type in a search string and matching it with a valid city (or area) boundary e.g. "Vancouver", while MapBox.jsx
renders a full page map and displays the city boundary based on the boundary coordinates retrieved from the Nominatim API.
In the parent component, App.jsx
, we start with const [coordinates, setCoordinates] = useState([]);
, which is a React hook that declares the coordinates
global variable and a function to update it, called setCoordinates
. coordinates
is an array of latitude and longitude pairs that make up the city boundary.
/* components/App.jsx */
function App() {
const [coordinates, setCoordinates] = useState([]);
return (
<div className="App">
<div className="search">
<CitySelect setCoordinates={setCoordinates} />
</div>
<div className="wrapper-map">
<MapBox coordinates={coordinates} />
</div>
</div>
);
}
export default App;
After that, we return JSX with the child <CitySelect/> and <MapBox/> components nested with in the parent <App/> component.
Autocomplete search bar
The <CitySearch/> search bar, located in the top left corner, allows users to search for and select administrative boundaries from the Nominatim database. It is built on top of the <Select/>
component from the Ant Design UI library (API reference) and includes an autocomplete feature which provides boundary name suggestions as the user types. For instance, typing "vancouver" will display several matching results.
To enhance performance and reduce excessive API calls, the search bar features a debouncing mechanism that introduces a brief delay after typing, ensuring that the API call to Nominatim is only made once the user has completed entering their search query. This is especially important because our app uses the rate-limited free version (https://nominatim.openstreetmap.org/search.php) of the Nominatim API. If too many API calls are made in a short period, our app could be blocked.
CitySelect.jsx
Here's a deep dive into what goes on in CitySelect.jsx
/* components/CitySelect.jsx */
<Select
showSearch
filterOption={false}
value={selectedCity}
placeholder="Select city"
onSearch={debounceFetchListCity}
notFoundContent={fetching ? <Spin size="small" /> : null}
onChange={setSelectedCity}
options={options}
style={{ width: "100%" }}
/>;
The code above outlines the main functionality of the <CitySelect> component. Let's explore what happens behind the scenes when a user types in a city name, selects one from the list of matching terms, and clicks to confirm their choice.
onSearch={debounceFetchListCity}
gets called when the user starts typing in the search input text box. The associated debounceFetchListCity()
method then retrieves a list of cities (areas) that match the search text input.
/* components/CitySelect.jsx */
const debounceFetchListCity = debounce(async (searchValue) => {
setListCity([]);
setOptions([]);
if (searchValue?.length > 5) {
try {
setFetching(true);
const response = await fetch(
`https://nominatim.openstreetmap.org/search.php?q=${searchValue}&polygon_geojson=1&format=json`,
);
const body = await response.json();
if (Array.isArray(body)) {
setListCity(body);
const options = body
.filter((result) => result.geojson?.type === "Polygon")
.map((result) => ({
label: result.display_name,
value: result.place_id,
}));
setOptions(options);
}
} catch (error) {
console.error("Error fetching city list:", error);
} finally {
setFetching(false);
}
}
}, 800);
In this method, we first wait for a 800 millisecond (0.8 second) pause in typing before calling the /search endpoint of the Nominatim API. If there's a match, the endpoint returns an array of matching place
objects. For example, the text string "vancouver" returns an array of three objects:
options={options}
provides the list of options that will be displayed in the dropdown of the search bar. options
is mapped from the array returned by the Nominatim API. Each option
object has a label
and value
that corresponds to result.display_name
and result.place_id
respectively.
onChange={setSelectedCity}
gets called when the user selects a city. If the selectedCity
variable changes, the useEffect()
hook is called and this in turn updates the global coordinates
variable with the latitude and longitude pairs that make up the boundary of newly chosen city.
/* components/CitySelect.jsx */
useEffect(() => {
if (selectedCity) {
const newCoordinates = listCities.find((el) => el.place_id === selectedCity)
?.geojson?.coordinates;
setCoordinates(newCoordinates || []);
}
// console.log("new coords:" + newCoordinates);
}, [selectedCity]);
MapBox.jsx
MapBox.jsx
handles the map and boundary (polygon) rendering. As with my previous tutorials, I've decided to go with Mapbox for the map layer used in this project but rather than go with its standard Javascript API, Mapbox GL JS, I've chosen to use the third party react-map-gl wrapper so that I can easily inject data and style our map using Source and Layer components like so:
<Map>
<Source>
<Layer/>
</Source>
</Map>
When the coordinates
array is updated (e.g. when the user selects a new city from the search bar CitySelect.jsx
), the MapBox.jsx
component is redrawn with new boundary data injected into the <Source/> component as a GeoJSON polygon, centered at its mid point.
<Source
type="geojson"
data={{
type: "Feature",
geometry: {
type: "Polygon",
coordinates: coordinates,
},
}}
>
<Layer
{...{
id: "layer-fill",
type: "fill",
paint: {
"fill-outline-color": "white",
"fill-color": "#E14C48",
"fill-opacity": 0.15,
},
}}
/>
<Layer
{...{
id: "layer-line",
type: "line",
paint: {
"line-color": "#000",
"line-width": 3,
},
}}
/>
</Source>;
The borders of the polygon are thick ("line-width": 3) black ("line-color": #000) with a light pink ("fill-color": "#E14C48", "fill-opacity": 0.15) fill.
To run this demo locally on your machine, clone this project's repository and in your terminal, run npm install
to install dependencies followed by npm start
to run the app. Don't forget to add your Mapbox API key to the .env file.
Spin up localhost:3000 on your favorite browser and if everything worked correctly, you should be able to see a map of Singapore. Type in "vancouver" and select the option "Vancouver, Metro Vancouver Regional District, British Columbia, Canada" and the map should show an outline of metro Vancouver in light pink. Type in the name of another city or area e.g. "University of British Columbia" to switch to a different boundary.
If you can't find the city or area that you are looking for, this means that it probably does not exist in the Nominatim database. Nominatim, like all OpenStreetMap projects, relies on volunteer contributions. Missing or incomplete data is quite common.
Wrap up and next steps
This concludes our tutorial on the OSM Nominatim API. We covered the basics of setting Nominatim up as a web service, making geocoding and reverse geocoding requests, and in this post, using Nominatim to draw and view boundary data.
There are many mapping services on the market - Google Maps, Mapbox and HERE Maps, just to name a few. The OSM Nominatim API could be a worthwhile, low cost alternative for your project if you are willing to put in the work to run and host it on your own servers, and understand the strengths and limitations of using open source OSM data.
Part 1: OpenStreetMap OSM Nominatim API tutorial
Part 2: Run a free geocoder with Nominatim Docker
Part 3: Building a free geocoding and reverse geocoding service with OpenStreetMap
Part 4: How to draw and view boundary data with OpenStreetMap OSM
👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.