
Library determines geographic region based on coordinates efficiently. Offers customizable data imports, handles subdivisions, and optimizes query speed through rasterized boundary data.
Kotlin multiplatform library to find in which region a given geo position is located fast.
© 2018-2025 Tobias Zwick. This library is released under the terms of the GNU Lesser General Public License (LGPL). The default data used is derived from OpenStreetMap and thus © OpenStreetMap contributors and licensed under the Open Data Commons Open Database License (ODbL).
Add de.westnordost:countryboundaries:3.0.0 as a Maven dependency or download the jar from there.
// load data. You should do this once and use CountryBoundaries as a singleton.
val source = FileSystem.source("boundaries.ser").buffered()
val boundaries = source.use { CountryBoundaries.deserializeFrom(it) }
// get ids of regions at position
boundaries.getIds(
longitude = -96.7954,
latitude = 32.7816
) // returns "US-TX","US"
// check if a position is in region with specified id
boundaries.isIn(
longitude = 8.6910,
latitude = 47.6973,
id = "DE"
) // returns true
// get ids of the regions that are present in the given bounds
boundaries.getIntersectingIds(
minLongitude = 50.6,
minLatitude = 5.9,
maxLongitude = 50.8,
maxLatitude = 6.1
) // returns "NL", "LU", "DE", "BE", "BE-VLG", "BE-WAL"
// get ids of the regions that completely cover the given bounds
boundaries.getContainingIds(
minLongitude = 50.6,
minLatitude = 5.9,
maxLongitude = 50.8,
maxLatitude = 6.1
) // returns empty listOn Java, use this to load the boundaries:
CountryBoundaries boundaries = null;
try (FileInputStream fis = new FileInputStream("boundaries.ser")) {
boundaries = CountryBoundariesUtils.deserializeFrom(fis);
}The default data file is in /data/. Don't forget to give attribution when distributing it. See below.
What exactly is returned when calling getIds is dependent on the source data used. The default data in /data/ is generated from this file in the JOSM project. It...
You can import own data from a GeoJson or an OSM XML, using the Java application in the /generator/ folder. This is also useful if you want to have custom raster sizes. What are rasters? See below.
Using the default data, on a Samsung S10e (Android phone from 2019), querying a single location takes something between 0.02 to 0.06 milliseconds. Querying 1 million random locations on a single thread takes about 0.5 seconds, with a Ryzen 5700X CPU (still single thread) about one quarter of that.
What makes it that fast is because the boundaries of the source data are split up into a raster. For the above measurements, I used a raster of 360x180 (= one cell is 1° in longitude, 1° in latitude). You can choose a smaller raster to have a smaller file or choose a bigger raster to have faster queries. According to my tests, a file with a raster of 60x30 (= one cell is 6° in longitude and latitude) is about 4 times smaller but queries are about 4 times slower.
Files with a raster of 60x30, 180x90 and 360x180 with the default data are supplied in /data/ but as explained in the above section, you can create files with custom raster sizes.
The reason why the library does not directly consume a GeoJSON or similar but only a file generated from it is so that the slicing of the source geometry into a raster does not need to be done each time the file is loaded but only once before putting the current version of the boundaries into the distribution.
Kotlin multiplatform library to find in which region a given geo position is located fast.
© 2018-2025 Tobias Zwick. This library is released under the terms of the GNU Lesser General Public License (LGPL). The default data used is derived from OpenStreetMap and thus © OpenStreetMap contributors and licensed under the Open Data Commons Open Database License (ODbL).
Add de.westnordost:countryboundaries:3.0.0 as a Maven dependency or download the jar from there.
// load data. You should do this once and use CountryBoundaries as a singleton.
val source = FileSystem.source("boundaries.ser").buffered()
val boundaries = source.use { CountryBoundaries.deserializeFrom(it) }
// get ids of regions at position
boundaries.getIds(
longitude = -96.7954,
latitude = 32.7816
) // returns "US-TX","US"
// check if a position is in region with specified id
boundaries.isIn(
longitude = 8.6910,
latitude = 47.6973,
id = "DE"
) // returns true
// get ids of the regions that are present in the given bounds
boundaries.getIntersectingIds(
minLongitude = 50.6,
minLatitude = 5.9,
maxLongitude = 50.8,
maxLatitude = 6.1
) // returns "NL", "LU", "DE", "BE", "BE-VLG", "BE-WAL"
// get ids of the regions that completely cover the given bounds
boundaries.getContainingIds(
minLongitude = 50.6,
minLatitude = 5.9,
maxLongitude = 50.8,
maxLatitude = 6.1
) // returns empty listOn Java, use this to load the boundaries:
CountryBoundaries boundaries = null;
try (FileInputStream fis = new FileInputStream("boundaries.ser")) {
boundaries = CountryBoundariesUtils.deserializeFrom(fis);
}The default data file is in /data/. Don't forget to give attribution when distributing it. See below.
What exactly is returned when calling getIds is dependent on the source data used. The default data in /data/ is generated from this file in the JOSM project. It...
You can import own data from a GeoJson or an OSM XML, using the Java application in the /generator/ folder. This is also useful if you want to have custom raster sizes. What are rasters? See below.
Using the default data, on a Samsung S10e (Android phone from 2019), querying a single location takes something between 0.02 to 0.06 milliseconds. Querying 1 million random locations on a single thread takes about 0.5 seconds, with a Ryzen 5700X CPU (still single thread) about one quarter of that.
What makes it that fast is because the boundaries of the source data are split up into a raster. For the above measurements, I used a raster of 360x180 (= one cell is 1° in longitude, 1° in latitude). You can choose a smaller raster to have a smaller file or choose a bigger raster to have faster queries. According to my tests, a file with a raster of 60x30 (= one cell is 6° in longitude and latitude) is about 4 times smaller but queries are about 4 times slower.
Files with a raster of 60x30, 180x90 and 360x180 with the default data are supplied in /data/ but as explained in the above section, you can create files with custom raster sizes.
The reason why the library does not directly consume a GeoJSON or similar but only a file generated from it is so that the slicing of the source geometry into a raster does not need to be done each time the file is loaded but only once before putting the current version of the boundaries into the distribution.