Repository: valhalla/valhalla
Pull request #5204: perf: Replace `boost::geometry` by GEOS for operations with admin/tz polygons and clip them by tile bbox
Author: login: kinkard
Created: 2025-04-18T20:24:18Z | Merged: 2025-04-29T20:12:35Z

# Issue

Use GEOS instead of `boost::geometry` for operations with admin and tz polygons, inspired by https://github.com/valhalla/valhalla/pull/5193#discussion_r2048315797

With this PR, the `BuildLocalTiles` timing on my M3 Max with europe-latest.osm.pbf (30GB) reduced from 1998s to 554s.

Major performance improvement (on systems that are not limited by filesystem read speed) comes from 
- clipping admin/tz polygons by tile bbox
- direct reading WKB from sqlite without intermediate transformation to WKT that was required for `boost::geometry`

Because of GEOS's [thread safe API](https://libgeos.org/usage/c_api/#reentrantthreadsafe-api), that require passing a context everywhere, noticeable amount of refactoring is required around interaction with admin/tz dbs - see new `AdminDB` class for more details.

## Tasklist

 - [x] Add tests
 - [ ] Add #fixes with the issue number that this PR addresses
 - [ ] Update the docs with any new request parameters or changes to behavior described
 - [x] Update the [changelog](CHANGELOG.md)
 - [ ] If you made changes to the lua files, update the [taginfo](taginfo.json) too.

## Requirements / Relations

 Link any requirements here. Other pull requests this PR is based on?

- first attempt to clip admin polygons with `boost::geometry` was taken in https://github.com/valhalla/valhalla/pull/5193 and during https://github.com/valhalla/valhalla/pull/5193#discussion_r2048315797 discussion I jumped into complete migration to GEOS
- some from the current PR were moved into https://github.com/valhalla/valhalla/pull/5206 that sets up concept of object to partially incapsulate handles, needed to work with admin db. Current PR extends that object to have GEOS context and WKB reader in a single place.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3c1dadd82a..e810d67713 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -64,6 +64,7 @@
    * CHANGED: Make static factor vectors/arrays in sif constexpr [#5200](https://github.com/valhalla/valhalla/pull/5200)
    * ADDED: Sqlite3 RAII wrapper around sqlite3* and spatielite connection [#5206](https://github.com/valhalla/valhalla/pull/5206)
    * CHANGED: Improved SQL statements when building admins [#5219](https://github.com/valhalla/valhalla/pull/5219)
+   * CHANGED: Replace `boost::geometry` by GEOS for operations with admin/tz polygons and clip them by tile bbox [#5204](https://github.com/valhalla/valhalla/pull/5204)
 
 ## Release Date: 2024-10-10 Valhalla 3.5.1
 * **Removed**
diff --git a/src/mjolnir/admin.cc b/src/mjolnir/admin.cc
index bfc92c1b15..17a20b59e6 100644
--- a/src/mjolnir/admin.cc
+++ b/src/mjolnir/admin.cc
@@ -2,20 +2,74 @@
 #include "baldr/datetime.h"
 #include "mjolnir/util.h"
 
+#include <geos_c.h>
+#include <sqlite3.h>
+
 #include <unordered_map>
 
 namespace valhalla {
 namespace mjolnir {
 
+namespace {
+
+// Tiles might have nodes slightly off the tile boundary. As for performance optimization the returned
+// geometry is clipped, a small buffer around should be added to handle edge cases.
+constexpr double kTileBboxBuffer = 1e-3;
+
+} // namespace
+
+Geometry::Geometry(geos_context_type ctx, GEOSGeometry* geom)
+    : context(std::move(ctx)), geometry(geom) {
+  prepared = GEOSPrepare_r(context.get(), geometry);
+}
+
+Geometry::~Geometry() {
+  GEOSPreparedGeom_destroy_r(context.get(), prepared);
+  GEOSGeom_destroy_r(context.get(), geometry);
+}
+
+bool Geometry::intersects(const PointLL& ll) const {
+  return GEOSPreparedIntersectsXY_r(context.get(), prepared, ll.lng(), ll.lat());
+}
+
+Geometry Geometry::clone() const {
+  return Geometry(context, GEOSGeom_clone_r(context.get(), geometry));
+}
+
+AdminDB::AdminDB(Sqlite3&& sqlite3)
+    : db(std::move(sqlite3)), geos_context(geos_context_type(GEOS_init_r(), GEOS_finish_r)) {
+  wkb_reader = GEOSWKBReader_create_r(geos_context.get());
+}
+
+AdminDB::~AdminDB() {
+  GEOSWKBReader_destroy_r(geos_context.get(), wkb_reader);
+}
+
+std::optional<AdminDB> AdminDB::open(const std::string& path) {
+  auto db = Sqlite3::open(path, SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX);
+  if (db) {
+    return AdminDB(std::move(*db));
+  }
+  return {};
+}
+
+Geometry
+AdminDB::read_wkb_and_clip(const unsigned char* wkb_blob, int wkb_size, const AABB2<PointLL>& bbox) {
+  GEOSGeometry* geom = GEOSWKBReader_read_r(geos_context.get(), wkb_reader, wkb_blob, wkb_size);
+  GEOSGeometry* clipped =
+      GEOSClipByRect_r(geos_context.get(), geom, bbox.minx(), bbox.miny(), bbox.maxx(), bbox.maxy());
+  GEOSGeom_destroy_r(geos_context.get(), geom);
+  return Geometry(geos_context, clipped);
+}
+
 // Get the polygon index.  Used by tz and admin areas.  Checks if the pointLL is covered_by the
 // poly.
-uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys,
+uint32_t GetMultiPolyId(const std::multimap<uint32_t, Geometry>& polys,
                         const PointLL& ll,
                         GraphTileBuilder& graphtile) {
   uint32_t index = 0;
-  point_type p(ll.lng(), ll.lat());
   for (const auto& poly : polys) {
-    if (bg::covered_by(p, poly.second, bg::strategy::within::crossings_multiply<point_type>())) {
+    if (poly.second.intersects(ll)) {
       const auto& admin = graphtile.admins_builder(poly.first);
       if (!admin.state_offset())
         index = poly.first;
@@ -28,16 +82,13 @@ uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys
 
 // Get the polygon index.  Used by tz and admin areas.  Checks if the pointLL is covered_by the
 // poly.
-uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys, const PointLL& ll) {
-  uint32_t index = 0;
-  point_type p(ll.lng(), ll.lat());
-  for (const auto& poly : polys) {
-    // TODO: we recently discovered that boost::geometry doesn't do bbox checks to speed things up
-    if (bg::covered_by(p, poly.second, bg::strategy::within::crossings_multiply<point_type>())) {
-      return poly.first;
+uint32_t GetMultiPolyId(const std::multimap<uint32_t, Geometry>& polys, const PointLL& ll) {
+  for (const auto& [index, poly] : polys) {
+    if (poly.intersects(ll)) {
+      return index;
     }
   }
-  return index;
+  return 0; // default index
 }
 
 // This function returns a vector pairs.  The pair is a string and boolean {language,
@@ -56,14 +107,8 @@ std::vector<std::pair<std::string, bool>> GetMultiPolyIndexes(const language_pol
   // first entry is blank for the default name
   languages.emplace_back("", false);
 
-  point_type p(ll.lng(), ll.lat());
-
-  for (auto it = polys.qbegin(bg::index::covers(p)); it != polys.qend(); ++it) {
-    // Make sure the point is in the admin poly (not just its envelope)
-    if (bg::covered_by(p, std::get<1>(*it), bg::strategy::within::crossings_multiply<point_type>())) {
-      auto& langs = std::get<2>(*it);
-      bool is_default = std::get<3>(*it);
-
+  for (const auto& [poly, langs, is_default] : polys) {
+    if (poly.intersects(ll)) {
       for (const auto& l : langs) {
         if (stringLanguage(l) != Language::kNone) {
           auto needle =
@@ -84,20 +129,22 @@ std::vector<std::pair<std::string, bool>> GetMultiPolyIndexes(const language_pol
 }
 
 // Get the timezone polys from the db
-std::multimap<uint32_t, multi_polygon_type> GetTimeZones(Sqlite3& db, const AABB2<PointLL>& aabb) {
-  std::multimap<uint32_t, multi_polygon_type> polys;
+std::multimap<uint32_t, Geometry> GetTimeZones(AdminDB& db, const AABB2<PointLL>& aabb) {
+  const AABB2<PointLL> bbox(aabb.minx() - kTileBboxBuffer, aabb.miny() - kTileBboxBuffer,
+                            aabb.maxx() + kTileBboxBuffer, aabb.maxy() + kTileBboxBuffer);
+  std::multimap<uint32_t, Geometry> polys;
   sqlite3_stmt* stmt = 0;
   uint32_t ret;
   uint32_t result = 0;
 
-  std::string sql = "select TZID, st_astext(geom) as geom_text from tz_world where ";
-  sql += "ST_Intersects(geom, BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) ";
+  std::string sql = "select TZID, ST_AsBinary(geom) as geom_text from tz_world where ";
+  sql += "ST_Intersects(geom, BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) ";
   sql += "and rowid IN (SELECT rowid FROM SpatialIndex WHERE f_table_name = ";
-  sql += "'tz_world' AND search_frame = BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + "));";
+  sql += "'tz_world' AND search_frame = BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + "));";
 
   ret = sqlite3_prepare_v2(db.get(), sql.c_str(), sql.length(), &stmt, 0);
 
@@ -106,13 +153,15 @@ std::multimap<uint32_t, multi_polygon_type> GetTimeZones(Sqlite3& db, const AABB
 
     while (result == SQLITE_ROW) {
       std::string tz_id;
-      std::string geom;
+      const unsigned char* wkb_blob = nullptr;
+      int wkb_size = 0;
 
       if (sqlite3_column_type(stmt, 0) == SQLITE_TEXT) {
         tz_id = (char*)sqlite3_column_text(stmt, 0);
       }
-      if (sqlite3_column_type(stmt, 1) == SQLITE_TEXT) {
-        geom = (char*)sqlite3_column_text(stmt, 1);
+      if (sqlite3_column_type(stmt, 1) == SQLITE_BLOB) {
+        wkb_blob = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 1));
+        wkb_size = sqlite3_column_bytes(stmt, 1);
       }
 
       uint32_t idx = DateTime::get_tz_db().to_index(tz_id);
@@ -121,9 +170,8 @@ std::multimap<uint32_t, multi_polygon_type> GetTimeZones(Sqlite3& db, const AABB
         throw std::runtime_error("Can't find timezone ID " + std::string(tz_id));
       }
 
-      multi_polygon_type multi_poly;
-      boost::geometry::read_wkt(geom, multi_poly);
-      polys.emplace(idx, multi_poly);
+      auto geom = db.read_wkb_and_clip(wkb_blob, wkb_size, bbox);
+      polys.emplace(idx, std::move(geom));
       result = sqlite3_step(stmt);
     }
   }
@@ -146,14 +194,16 @@ std::vector<std::string> ParseLanguageTokens(const std::string& lang_tag) {
   return langs;
 }
 
-void GetData(Sqlite3& db,
+// Get the admin data from the spatialite db given an SQL statement
+void GetData(AdminDB& db,
              sqlite3_stmt* stmt,
              const std::string& sql,
+             const AABB2<PointLL>& bbox,
              GraphTileBuilder& tilebuilder,
-             std::multimap<uint32_t, multi_polygon_type>& polys,
+             std::multimap<uint32_t, Geometry>& polys,
              std::unordered_map<uint32_t, bool>& drive_on_right,
              std::unordered_map<uint32_t, bool>& allow_intersection_names,
-             language_poly_index& language_ploys,
+             language_poly_index& language_polys,
              bool languages_only = false) {
   uint32_t result = 0;
   bool dor = true;
@@ -211,29 +261,30 @@ void GetData(Sqlite3& db,
         default_language = (char*)sqlite3_column_text(stmt, 8);
       }
 
-      std::string geom;
-      if (sqlite3_column_type(stmt, 9) == SQLITE_TEXT) {
-        geom = (char*)sqlite3_column_text(stmt, 9);
+      const unsigned char* wkb_blob = nullptr;
+      int wkb_size = 0;
+      if (sqlite3_column_type(stmt, 9) == SQLITE_BLOB) {
+        wkb_blob = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 9));
+        wkb_size = sqlite3_column_bytes(stmt, 9);
       }
 
       uint32_t index = tilebuilder.AddAdmin(country_name, state_name, country_iso, state_iso);
-      multi_polygon_type multi_poly;
-      bg::read_wkt(geom, multi_poly);
-      polys.emplace(index, multi_poly);
-      drive_on_right.emplace(index, dor);
-      allow_intersection_names.emplace(index, intersection_name);
 
-      bg::model::box<point_type> box{};
-      bg::envelope(multi_poly, box);
+      auto geom = db.read_wkb_and_clip(wkb_blob, wkb_size, bbox);
+
       if (!default_language.empty()) {
         auto langs = ParseLanguageTokens(default_language);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, true));
+        language_polys.push_back(std::make_tuple(geom.clone(), std::move(langs), true));
       }
       if (!supported_languages.empty()) {
         auto langs = ParseLanguageTokens(supported_languages);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, false));
+        language_polys.push_back(std::make_tuple(geom.clone(), std::move(langs), false));
       }
 
+      polys.emplace(index, std::move(geom));
+      drive_on_right.emplace(index, dor);
+      allow_intersection_names.emplace(index, intersection_name);
+
     } else {
 
       std::string supported_languages;
@@ -246,23 +297,22 @@ void GetData(Sqlite3& db,
         default_language = (char*)sqlite3_column_text(stmt, 2);
       }
 
-      std::string geom;
-      if (sqlite3_column_type(stmt, 3) == SQLITE_TEXT) {
-        geom = (char*)sqlite3_column_text(stmt, 3);
+      const unsigned char* wkb_blob = nullptr;
+      int wkb_size = 0;
+      if (sqlite3_column_type(stmt, 3) == SQLITE_BLOB) {
+        wkb_blob = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 3));
+        wkb_size = sqlite3_column_bytes(stmt, 3);
       }
 
-      multi_polygon_type multi_poly;
-      bg::read_wkt(geom, multi_poly);
-      bg::model::box<point_type> box{};
-      bg::envelope(multi_poly, box);
+      auto geom = db.read_wkb_and_clip(wkb_blob, wkb_size, bbox);
 
       if (!default_language.empty()) {
         auto langs = ParseLanguageTokens(default_language);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, true));
+        language_polys.push_back(std::make_tuple(geom.clone(), std::move(langs), true));
       }
       if (!supported_languages.empty()) {
         auto langs = ParseLanguageTokens(supported_languages);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, false));
+        language_polys.push_back(std::make_tuple(std::move(geom), std::move(langs), false));
       }
     }
 
@@ -276,59 +326,62 @@ void GetData(Sqlite3& db,
 }
 
 // Get the admin polys that intersect with the tile bounding box.
-std::multimap<uint32_t, multi_polygon_type>
-GetAdminInfo(Sqlite3& db,
+std::multimap<uint32_t, Geometry>
+GetAdminInfo(AdminDB& db,
              std::unordered_map<uint32_t, bool>& drive_on_right,
              std::unordered_map<uint32_t, bool>& allow_intersection_names,
              language_poly_index& language_polys,
              const AABB2<PointLL>& aabb,
              GraphTileBuilder& tilebuilder) {
-  std::multimap<uint32_t, multi_polygon_type> polys;
+  const AABB2<PointLL> bbox(aabb.minx() - kTileBboxBuffer, aabb.miny() - kTileBboxBuffer,
+                            aabb.maxx() + kTileBboxBuffer, aabb.maxy() + kTileBboxBuffer);
+
+  std::multimap<uint32_t, Geometry> polys;
   sqlite3_stmt* stmt = 0;
 
   // default language query
   std::string sql =
-      "SELECT admin_level, supported_languages, default_language, st_astext(geom) from ";
+      "SELECT admin_level, supported_languages, default_language, ST_AsBinary(geom) from ";
   sql +=
       " admins where (supported_languages is NOT NULL or default_language is NOT NULL) and ST_Intersects(geom, BuildMBR(" +
-      std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) and admin_level>4 ";
+      std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) and admin_level>4 ";
   sql += "and rowid IN (SELECT rowid FROM SpatialIndex WHERE f_table_name = ";
-  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) order by admin_level desc, name;";
-  GetData(db, stmt, sql, tilebuilder, polys, drive_on_right, allow_intersection_names, language_polys,
-          true);
+  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) order by admin_level desc, name;";
+  GetData(db, stmt, sql, bbox, tilebuilder, polys, drive_on_right, allow_intersection_names,
+          language_polys, true);
 
   // state query
   sql = "SELECT country.name, state.name, country.iso_code, ";
   sql += "state.iso_code, state.drive_on_right, state.allow_intersection_names, state.admin_level, ";
   sql +=
-      "state.supported_languages, state.default_language, st_astext(state.geom) from admins state, admins country where ";
-  sql += "ST_Intersects(state.geom, BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) and ";
+      "state.supported_languages, state.default_language, ST_AsBinary(state.geom) from admins state, admins country where ";
+  sql += "ST_Intersects(state.geom, BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) and ";
   sql += "country.rowid = state.parent_admin and state.admin_level=4 ";
   sql += "and state.rowid IN (SELECT rowid FROM SpatialIndex WHERE f_table_name = ";
-  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) order by state.name, country.name;";
-  GetData(db, stmt, sql, tilebuilder, polys, drive_on_right, allow_intersection_names,
+  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) order by state.name, country.name;";
+  GetData(db, stmt, sql, bbox, tilebuilder, polys, drive_on_right, allow_intersection_names,
           language_polys);
 
   // country query
   sql = "SELECT name, \"\", iso_code, \"\", drive_on_right, allow_intersection_names, admin_level, ";
   sql +=
-      "supported_languages, default_language, st_astext(geom) from  admins where ST_Intersects(geom, BuildMBR(" +
-      std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) and admin_level=2 ";
+      "supported_languages, default_language, ST_AsBinary(geom) from  admins where ST_Intersects(geom, BuildMBR(" +
+      std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) and admin_level=2 ";
   sql += "and rowid IN (SELECT rowid FROM SpatialIndex WHERE f_table_name = ";
-  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(aabb.minx()) + ",";
-  sql += std::to_string(aabb.miny()) + ", " + std::to_string(aabb.maxx()) + ",";
-  sql += std::to_string(aabb.maxy()) + ")) order by name;";
-  GetData(db, stmt, sql, tilebuilder, polys, drive_on_right, allow_intersection_names,
+  sql += "'admins' AND search_frame = BuildMBR(" + std::to_string(bbox.minx()) + ",";
+  sql += std::to_string(bbox.miny()) + ", " + std::to_string(bbox.maxx()) + ",";
+  sql += std::to_string(bbox.maxy()) + ")) order by name;";
+  GetData(db, stmt, sql, bbox, tilebuilder, polys, drive_on_right, allow_intersection_names,
           language_polys);
 
   if (stmt) { // just in case something bad happened.
@@ -339,7 +392,7 @@ GetAdminInfo(Sqlite3& db,
 }
 
 // Get all the country access records from the db and save them to a map.
-std::unordered_map<std::string, std::vector<int>> GetCountryAccess(Sqlite3& db) {
+std::unordered_map<std::string, std::vector<int>> GetCountryAccess(AdminDB& db) {
   std::unordered_map<std::string, std::vector<int>> country_access;
   sqlite3_stmt* stmt = 0;
   uint32_t ret;
diff --git a/src/mjolnir/adminbuilder.cc b/src/mjolnir/adminbuilder.cc
index 049758d512..4e47856354 100644
--- a/src/mjolnir/adminbuilder.cc
+++ b/src/mjolnir/adminbuilder.cc
@@ -1,9 +1,5 @@
-#include <cstdint>
-#include <string>
-#include <vector>
-
-#include "filesystem.h"
 #include "mjolnir/adminbuilder.h"
+#include "filesystem.h"
 #include "mjolnir/adminconstants.h"
 #include "mjolnir/pbfadminparser.h"
 #include "mjolnir/sqlite3.h"
@@ -14,6 +10,11 @@
 #include <boost/property_tree/ptree.hpp>
 
 #include <geos_c.h>
+#include <sqlite3.h>
+
+#include <cstdint>
+#include <string>
+#include <vector>
 
 BOOST_GEOMETRY_REGISTER_POINT_2D(valhalla::midgard::PointLL,
                                  double,
diff --git a/src/mjolnir/convert_transit.cc b/src/mjolnir/convert_transit.cc
index b027bfe768..b672a6dfc9 100644
--- a/src/mjolnir/convert_transit.cc
+++ b/src/mjolnir/convert_transit.cc
@@ -2,6 +2,7 @@
 #include <cstdint>
 #include <future>
 #include <memory>
+#include <set>
 #include <string>
 #include <thread>
 #include <unordered_set>
@@ -528,8 +529,7 @@ void AddToGraph(GraphTileBuilder& tilebuilder_transit,
                 const std::unordered_map<uint32_t, Shape>& shape_data,
                 const std::vector<float>& distances,
                 const std::vector<uint32_t>& route_types,
-                bool tile_within_one_tz,
-                const std::multimap<uint32_t, multi_polygon_type>& tz_polys,
+                const std::multimap<uint32_t, Geometry>& tz_polys,
                 uint32_t& no_dir_edge_count) {
   auto t1 = std::chrono::high_resolution_clock::now();
 
@@ -603,7 +603,7 @@ void AddToGraph(GraphTileBuilder& tilebuilder_transit,
       if (timezone == 0) {
         // fallback to tz database.
         timezone =
-            (tile_within_one_tz) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, station_ll);
+            (tz_polys.size() == 1) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, station_ll);
 
         if (timezone == 0) {
           LOG_WARN("Timezone not found for station " + station.name());
@@ -648,7 +648,7 @@ void AddToGraph(GraphTileBuilder& tilebuilder_transit,
         if (timezone == 0) {
           // fallback to tz database.
           timezone =
-              (tile_within_one_tz) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, egress_ll);
+              (tz_polys.size() == 1) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, egress_ll);
           if (timezone == 0) {
             LOG_WARN("Timezone not found for egress " + egress.name());
           }
@@ -825,7 +825,7 @@ void AddToGraph(GraphTileBuilder& tilebuilder_transit,
     if (timezone == 0) {
       // fallback to tz database.
       timezone =
-          (tile_within_one_tz) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, platform_ll);
+          (tz_polys.size() == 1) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, platform_ll);
       if (timezone == 0) {
         LOG_WARN("Timezone not found for platform " + platform.name());
       }
@@ -997,7 +997,7 @@ void build_tiles(const boost::property_tree::ptree& pt,
 
   GraphReader reader(pt);
   auto database = pt.get_optional<std::string>("timezone");
-  auto tz_db = Sqlite3::open(*database);
+  auto tz_db = AdminDB::open(*database);
   if (!tz_db) {
     LOG_WARN("Time zone db " + *database + " not found. Not saving time zone information from db.");
   }
@@ -1167,20 +1167,14 @@ void build_tiles(const boost::property_tree::ptree& pt,
 
     // Add routes to the tile. Get vector of route types.
     std::vector<uint32_t> route_types = AddRoutes(tile_pbf, tilebuilder_transit);
-    auto tile_bounds = tiles.TileBounds(tile_id.tileid());
-    bool tile_within_one_tz = false;
-    std::multimap<uint32_t, multi_polygon_type> tz_polys;
+    std::multimap<uint32_t, Geometry> tz_polys;
     if (tz_db) {
-      tz_polys = GetTimeZones(*tz_db, tile_bounds);
-      if (tz_polys.size() < 2) {
-        tile_within_one_tz = true;
-      }
+      tz_polys = GetTimeZones(*tz_db, tiles.TileBounds(tile_id.tileid()));
     }
 
     // Add nodes, directededges, and edgeinfo
     AddToGraph(tilebuilder_transit, tile_id, tile_pbf, transit_dir, lock, stop_edge_map,
-               stop_no_access, shapes, distances, route_types, tile_within_one_tz, tz_polys,
-               stats.no_dir_edge_count);
+               stop_no_access, shapes, distances, route_types, tz_polys, stats.no_dir_edge_count);
 
     LOG_INFO("Tile " + std::to_string(tile_id.tileid()) + ": added " +
              std::to_string(tile_pbf.nodes_size()) + " stops, " +
diff --git a/src/mjolnir/graphbuilder.cc b/src/mjolnir/graphbuilder.cc
index 3e3c5454b9..04a4904dea 100644
--- a/src/mjolnir/graphbuilder.cc
+++ b/src/mjolnir/graphbuilder.cc
@@ -433,7 +433,7 @@ void BuildTileSet(const std::string& ways_file,
   bool use_admin_db = pt.get<bool>("data_processing.use_admin_db", true);
 
   // Initialize the admin DB (if it exists)
-  auto admin_db = (database && use_admin_db) ? Sqlite3::open(*database) : std::optional<Sqlite3>{};
+  auto admin_db = (database && use_admin_db) ? AdminDB::open(*database) : std::optional<AdminDB>{};
   if (!database && use_admin_db) {
     LOG_WARN("Admin db not found. Not saving admin information.");
   } else if (!admin_db && use_admin_db) {
@@ -442,7 +442,7 @@ void BuildTileSet(const std::string& ways_file,
 
   database = pt.get_optional<std::string>("timezone");
   // Initialize the tz DB (if it exists)
-  auto tz_db = database ? Sqlite3::open(*database) : std::optional<Sqlite3>{};
+  auto tz_db = database ? AdminDB::open(*database) : std::optional<AdminDB>{};
   if (!database) {
     LOG_WARN("Time zone db not found.  Not saving time zone information.");
   } else if (!tz_db) {
@@ -502,8 +502,7 @@ void BuildTileSet(const std::string& ways_file,
 
       // Get the admin polygons. If only one exists for the tile check if the
       // tile is entirely inside the polygon
-      bool tile_within_one_admin = false;
-      std::multimap<uint32_t, multi_polygon_type> admin_polys;
+      std::multimap<uint32_t, Geometry> admin_polys;
       std::unordered_map<uint32_t, bool> drive_on_right;
       std::unordered_map<uint32_t, bool> allow_intersection_names;
       language_poly_index language_polys;
@@ -511,19 +510,11 @@ void BuildTileSet(const std::string& ways_file,
       if (admin_db) {
         admin_polys = GetAdminInfo(*admin_db, drive_on_right, allow_intersection_names,
                                    language_polys, tiling.TileBounds(id), graphtile);
-        if (admin_polys.size() == 1) {
-          // TODO - check if tile bounding box is entirely inside the polygon...
-          tile_within_one_admin = true;
-        }
       }
 
-      std::multimap<uint32_t, multi_polygon_type> tz_polys;
-      bool tile_within_one_tz = false;
+      std::multimap<uint32_t, Geometry> tz_polys;
       if (tz_db) {
         tz_polys = GetTimeZones(*tz_db, tiling.TileBounds(id));
-        if (tz_polys.size() == 1) {
-          tile_within_one_tz = true;
-        }
       }
 
       // Iterate through the nodes
@@ -558,8 +549,8 @@ void BuildTileSet(const std::string& ways_file,
         std::vector<std::pair<std::string, bool>> default_languages;
 
         if (use_admin_db) {
-          admin_index = (tile_within_one_admin) ? admin_polys.begin()->first
-                                                : GetMultiPolyId(admin_polys, node_ll, graphtile);
+          admin_index = (admin_polys.size() == 1) ? admin_polys.begin()->first
+                                                  : GetMultiPolyId(admin_polys, node_ll, graphtile);
           dor = drive_on_right[admin_index];
           default_languages = GetMultiPolyIndexes(language_polys, node_ll);
 
@@ -1281,7 +1272,7 @@ void BuildTileSet(const std::string& ways_file,
 
         // Set the time zone index
         uint32_t tz_index =
-            (tile_within_one_tz) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, node_ll);
+            (tz_polys.size() == 1) ? tz_polys.begin()->first : GetMultiPolyId(tz_polys, node_ll);
 
         graphtile.nodes().back().set_timezone(tz_index);
 
@@ -1489,9 +1480,6 @@ void GraphBuilder::Build(const boost::property_tree::ptree& pt,
                pt.get<unsigned int>("mjolnir.concurrency", std::thread::hardware_concurrency()));
 
   auto tile_dir = pt.get<std::string>("mjolnir.tile_dir");
-  // Disable sqlite3 internal memory tracking (results in a high-contention mutex, and we don't care
-  // about marginal sqlite memory usage).
-  sqlite3_config(SQLITE_CONFIG_MEMSTATUS, false);
 
   BuildLocalTiles(threads, osmdata, ways_file, way_nodes_file, nodes_file, edges_file,
                   complex_from_restriction_file, complex_to_restriction_file, linguistic_node_file,
diff --git a/src/mjolnir/graphenhancer.cc b/src/mjolnir/graphenhancer.cc
index b55c3faa1b..3e544d3d24 100644
--- a/src/mjolnir/graphenhancer.cc
+++ b/src/mjolnir/graphenhancer.cc
@@ -1290,7 +1290,7 @@ void enhance(const boost::property_tree::ptree& pt,
   bool use_urban_tag = pt.get<bool>("data_processing.use_urban_tag", false);
   bool use_admin_db = pt.get<bool>("data_processing.use_admin_db", true);
   // Initialize the admin DB (if it exists)
-  auto admin_db = (database && use_admin_db) ? Sqlite3::open(*database) : std::optional<Sqlite3>{};
+  auto admin_db = (database && use_admin_db) ? AdminDB::open(*database) : std::optional<AdminDB>{};
   if (!database && use_admin_db) {
     LOG_WARN("Admin db not found. Not saving admin information.");
   } else if (!admin_db && use_admin_db) {
diff --git a/src/mjolnir/ingest_transit.cc b/src/mjolnir/ingest_transit.cc
index f63842b667..ebaf18639f 100644
--- a/src/mjolnir/ingest_transit.cc
+++ b/src/mjolnir/ingest_transit.cc
@@ -5,6 +5,7 @@
 #include <memory>
 #include <mutex>
 #include <queue>
+#include <set>
 #include <sstream>
 #include <string>
 #include <thread>
@@ -1040,4 +1041,4 @@ void write_pbf(const Transit& tile, const filesystem::path& transit_tile) {
 }
 
 } // namespace mjolnir
-} // namespace valhalla
\ No newline at end of file
+} // namespace valhalla
diff --git a/src/mjolnir/landmarks.cc b/src/mjolnir/landmarks.cc
index f854e1086b..757fc89b57 100644
--- a/src/mjolnir/landmarks.cc
+++ b/src/mjolnir/landmarks.cc
@@ -14,6 +14,7 @@
 #include "sif/nocost.h"
 
 #include <osmium/io/pbf_input.hpp>
+#include <sqlite3.h>
 
 #include <future>
 #include <string_view>
diff --git a/src/mjolnir/sqlite3.cc b/src/mjolnir/sqlite3.cc
index eeda363fad..8fabdef2e9 100644
--- a/src/mjolnir/sqlite3.cc
+++ b/src/mjolnir/sqlite3.cc
@@ -2,12 +2,28 @@
 #include "midgard/logging.h"
 #include "valhalla/filesystem.h"
 
+#include <sqlite3.h>
+// needs to be after sqlite include
 #include <spatialite.h>
 
 #include <mutex>
 
 namespace {
 
+struct sqlite_singleton_t {
+  static const sqlite_singleton_t& get_instance() {
+    static sqlite_singleton_t s;
+    return s;
+  }
+
+private:
+  sqlite_singleton_t() {
+    // Disable sqlite3 internal memory tracking (results in a high-contention mutex, and we don't care
+    // about marginal sqlite memory usage).
+    sqlite3_config(SQLITE_CONFIG_MEMSTATUS, false);
+  }
+};
+
 struct spatialite_singleton_t {
   static const spatialite_singleton_t& get_instance() {
     static spatialite_singleton_t s;
@@ -37,6 +53,8 @@ std::optional<Sqlite3> Sqlite3::open(const std::string& path, int flags) {
     return {};
   }
 
+  sqlite_singleton_t::get_instance();
+
   sqlite3* db = nullptr;
   uint32_t ret = sqlite3_open_v2(path.c_str(), &db, flags, nullptr);
   if (ret != SQLITE_OK) {
diff --git a/src/mjolnir/statistics_database.cc b/src/mjolnir/statistics_database.cc
index e52eeea665..81a209f30b 100644
--- a/src/mjolnir/statistics_database.cc
+++ b/src/mjolnir/statistics_database.cc
@@ -5,6 +5,8 @@
 #include "midgard/logging.h"
 #include "mjolnir/sqlite3.h"
 
+#include <sqlite3.h>
+
 using namespace valhalla::midgard;
 using namespace valhalla::baldr;
 using namespace valhalla::mjolnir;
diff --git a/src/mjolnir/valhalla_benchmark_admins.cc b/src/mjolnir/valhalla_benchmark_admins.cc
index 74a180c41e..57965c1de8 100644
--- a/src/mjolnir/valhalla_benchmark_admins.cc
+++ b/src/mjolnir/valhalla_benchmark_admins.cc
@@ -10,6 +10,7 @@
 #include <boost/geometry/io/wkt/wkt.hpp>
 #include <boost/property_tree/ptree.hpp>
 #include <cxxopts.hpp>
+#include <sqlite3.h>
 
 #include "baldr/admininfo.h"
 #include "baldr/graphconstants.h"
diff --git a/test/graphbuilder.cc b/test/graphbuilder.cc
index cb866c1218..41ea431c77 100644
--- a/test/graphbuilder.cc
+++ b/test/graphbuilder.cc
@@ -1,17 +1,14 @@
 #include "mjolnir/graphbuilder.h"
 #include "baldr/graphreader.h"
-#include "midgard/sequence.h"
 #include "mjolnir/admin.h"
 #include "mjolnir/directededgebuilder.h"
 #include "mjolnir/osmdata.h"
 #include "mjolnir/pbfgraphparser.h"
-#include "mjolnir/util.h"
-
-#include <string>
 
 #include <boost/property_tree/ptree.hpp>
+#include <gtest/gtest.h>
 
-#include "test.h"
+#include <string>
 
 using boost::property_tree::ptree;
 using namespace valhalla;
@@ -19,8 +16,6 @@ using namespace valhalla::midgard;
 using namespace valhalla::mjolnir;
 using valhalla::baldr::GraphId;
 using valhalla::baldr::GraphReader;
-using valhalla::mjolnir::build_tile_set;
-using valhalla::mjolnir::TileManifest;
 
 #if !defined(VALHALLA_SOURCE_DIR)
 #define VALHALLA_SOURCE_DIR
@@ -129,7 +124,7 @@ class TestNodeInfo : NodeInfo {
 
 TEST(Graphbuilder, NewTimezones) {
   TestNodeInfo test_node;
-  auto sql_db = Sqlite3::open(VALHALLA_BUILD_DIR "test/data/tz.sqlite");
+  auto sql_db = AdminDB::open(VALHALLA_BUILD_DIR "test/data/tz.sqlite");
   ASSERT_TRUE(sql_db);
 
   const auto& tzdb = DateTime::get_tz_db();
@@ -149,6 +144,57 @@ TEST(Graphbuilder, NewTimezones) {
   EXPECT_EQ(test_node.get_raw_timezone_ext1_field(), 1);
 }
 
+TEST(Graphbuilder, AdminBbox) {
+  auto admin_db = AdminDB::open(VALHALLA_SOURCE_DIR "test/data/language_admin.sqlite");
+  ASSERT_TRUE(admin_db);
+
+  const auto& tiling = TileHierarchy::levels().back().tiles;
+
+  // Problematic tile in Belgium where boost::geometry::intersection(box, polygon) fails to produce
+  // multiple polygons.
+  const GraphId id(811462);
+  GraphTileBuilder graphtile(tile_dir, id, false);
+  std::unordered_map<uint32_t, bool> drive_on_right;
+  std::unordered_map<uint32_t, bool> allow_intersection_names;
+  language_poly_index language_polys;
+
+  const AABB2<PointLL> bbox = tiling.TileBounds(id);
+  auto admin_polys = GetAdminInfo(*admin_db, drive_on_right, allow_intersection_names, language_polys,
+                                  bbox, graphtile);
+
+  const auto admin_name = [&](const Admin& admin) {
+    return admin.country_iso() + "/" + admin.state_iso();
+  };
+
+  ASSERT_EQ(admin_polys.size(), 3);
+  EXPECT_EQ(admin_name(graphtile.admins_builder(0)), "/"); // default empty strings
+  EXPECT_EQ(admin_name(graphtile.admins_builder(1)), "BE/VLG");
+  EXPECT_EQ(admin_name(graphtile.admins_builder(2)), "BE/WAL");
+  EXPECT_EQ(admin_name(graphtile.admins_builder(3)), "BE/");
+
+  // fr, nl, nl, fr
+  ASSERT_EQ(language_polys.size(), 4);
+
+  // Ensure that tile corners are handled correctly
+  EXPECT_EQ(admin_name(graphtile.admins_builder(
+                GetMultiPolyId(admin_polys, PointLL(bbox.minx(), bbox.miny()), graphtile))),
+            "BE/VLG");
+  EXPECT_EQ(admin_name(graphtile.admins_builder(
+                GetMultiPolyId(admin_polys, PointLL(bbox.maxx(), bbox.miny()), graphtile))),
+            "BE/VLG");
+  EXPECT_EQ(admin_name(graphtile.admins_builder(
+                GetMultiPolyId(admin_polys, PointLL(bbox.minx(), bbox.maxy()), graphtile))),
+            "BE/VLG");
+  EXPECT_EQ(admin_name(graphtile.admins_builder(
+                GetMultiPolyId(admin_polys, PointLL(bbox.maxx(), bbox.maxy()), graphtile))),
+            "BE/VLG");
+
+  EXPECT_EQ(admin_name(graphtile.admins_builder(
+                GetMultiPolyId(admin_polys, PointLL((bbox.minx() + bbox.maxx()) / 2, bbox.miny()),
+                               graphtile))),
+            "BE/WAL");
+}
+
 class HarrisburgTestSuiteEnv : public ::testing::Environment {
 public:
   void SetUp() override {
diff --git a/test/gurka/test_admin_sidewalk_crossing_override.cc b/test/gurka/test_admin_sidewalk_crossing_override.cc
index ea68ec9aee..476009eac5 100644
--- a/test/gurka/test_admin_sidewalk_crossing_override.cc
+++ b/test/gurka/test_admin_sidewalk_crossing_override.cc
@@ -1,6 +1,7 @@
 #include <filesystem>
 
 #include <gtest/gtest.h>
+#include <sqlite3.h>
 
 #include "baldr/admin.h"
 #include "gurka.h"
diff --git a/test/gurka/test_admin_uk_override.cc b/test/gurka/test_admin_uk_override.cc
index 212cadcbe8..b62e81d32a 100644
--- a/test/gurka/test_admin_uk_override.cc
+++ b/test/gurka/test_admin_uk_override.cc
@@ -1,5 +1,6 @@
 #include <filesystem>
 #include <gtest/gtest.h>
+#include <sqlite3.h>
 
 #include "baldr/admin.h"
 #include "gurka.h"
diff --git a/test/gurka/test_build_admin.cc b/test/gurka/test_build_admin.cc
index 2c7c4789ea..fb2885c09e 100644
--- a/test/gurka/test_build_admin.cc
+++ b/test/gurka/test_build_admin.cc
@@ -1,6 +1,7 @@
 #include <filesystem>
 
 #include <gtest/gtest.h>
+#include <sqlite3.h>
 
 #include "baldr/admin.h"
 #include "gurka.h"
diff --git a/valhalla/mjolnir/admin.h b/valhalla/mjolnir/admin.h
index b276c7095d..d27c914291 100644
--- a/valhalla/mjolnir/admin.h
+++ b/valhalla/mjolnir/admin.h
@@ -1,35 +1,102 @@
 #ifndef VALHALLA_MJOLNIR_ADMIN_H_
 #define VALHALLA_MJOLNIR_ADMIN_H_
 
-#include <boost/geometry.hpp>
-#include <boost/geometry/geometries/multi_polygon.hpp>
-#include <boost/geometry/geometries/point_xy.hpp>
-#include <boost/geometry/geometries/polygon.hpp>
-#include <boost/geometry/io/wkt/wkt.hpp>
-
-#include <cstdint>
-#include <unordered_map>
 #include <valhalla/baldr/graphconstants.h>
 #include <valhalla/midgard/aabb2.h>
 #include <valhalla/midgard/pointll.h>
 #include <valhalla/mjolnir/graphtilebuilder.h>
 #include <valhalla/mjolnir/sqlite3.h>
 
+#include <cstdint>
+#include <unordered_map>
+
+struct GEOSContextHandle_HS;
+struct GEOSGeom_t;
+struct GEOSPrepGeom_t;
+struct GEOSWKBReader_t;
+
 using namespace valhalla::baldr;
 using namespace valhalla::midgard;
-namespace bg = boost::geometry;
 
 namespace valhalla {
 namespace mjolnir {
 
-// Geometry types for admin queries
-typedef bg::model::d2::point_xy<double> point_type;
-typedef bg::model::polygon<point_type> polygon_type;
-typedef bg::model::multi_polygon<polygon_type> multi_polygon_type;
-typedef bg::index::rtree<
-    std::tuple<bg::model::box<point_type>, multi_polygon_type, std::vector<std::string>, bool>,
-    bg::index::rstar<16>>
-    language_poly_index;
+// GEOS thread-safe API requires a context handle for each operation, that should be unique for every
+// thread. See https://libgeos.org/usage/c_api/#reentrantthreadsafe-api for more details.
+typedef std::shared_ptr<GEOSContextHandle_HS> geos_context_type;
+
+// RAII wrapper for GEOSGeometry
+struct Geometry {
+  geos_context_type context;
+  GEOSGeom_t* geometry;
+  const GEOSPrepGeom_t* prepared;
+
+  Geometry(geos_context_type ctx, GEOSGeom_t* geom);
+  ~Geometry();
+
+  // This class cannot be copied, but can be moved
+  Geometry(Geometry const&) = delete;
+  Geometry& operator=(Geometry const&) = delete;
+  Geometry(Geometry&& other) noexcept
+      : context(std::move(other.context)), geometry(other.geometry), prepared(other.prepared) {
+    other.geometry = nullptr;
+    other.prepared = nullptr;
+  }
+  Geometry& operator=(Geometry&& other) noexcept {
+    // move and swap idiom via local variable
+    Geometry local = std::move(other);
+    std::swap(geometry, local.geometry);
+    std::swap(context, local.context);
+    return *this;
+  }
+
+  // Returns true if the geometry intersects the given point
+  bool intersects(const PointLL& ll) const;
+  // Creates a clone of the current geometry
+  Geometry clone() const;
+};
+
+typedef std::vector<std::tuple<Geometry, std::vector<std::string>, bool>> language_poly_index;
+
+class AdminDB {
+  Sqlite3 db;
+  geos_context_type geos_context;
+  GEOSWKBReader_t* wkb_reader;
+
+  // Constructor is private, use `AdminDB::open()` instead.
+  AdminDB(Sqlite3&& db);
+
+public:
+  // Tries to open an AdminDB from the given path. Returns std::nullopt if failed.
+  static std::optional<AdminDB> open(const std::string& path);
+  ~AdminDB();
+
+  // This class cannot be copied, but can be moved
+  AdminDB(AdminDB const&) = delete;
+  AdminDB& operator=(AdminDB const&) = delete;
+  AdminDB(AdminDB&& other) noexcept
+      : db(std::move(other.db)), geos_context(std::move(other.geos_context)),
+        wkb_reader(other.wkb_reader) {
+    other.wkb_reader = nullptr;
+  }
+  AdminDB& operator=(AdminDB&& other) noexcept {
+    // move and swap idiom via local variable
+    AdminDB local = std::move(other);
+    std::swap(db, local.db);
+    std::swap(geos_context, local.geos_context);
+    std::swap(wkb_reader, local.wkb_reader);
+    return *this;
+  }
+
+  sqlite3* get() {
+    return db.get();
+  }
+
+  // Reads a WKB blob and clips it to the given bounding box.
+  // These two operations are combined into a single function to simplify amount of abstractions
+  // and leave `Geometry` always deal with GEOSPreparedGeometry without intermediate entity.
+  Geometry read_wkb_and_clip(const unsigned char* wkb_blob, int wkb_size, const AABB2<PointLL>& bbox);
+};
 
 /**
  * Get the polygon index.  Used by tz and admin areas.  Checks if the pointLL is covered_by the
@@ -38,7 +105,7 @@ typedef bg::index::rtree<
  * @param  ll         point that needs to be checked.
  * @param  graphtile  graphtilebuilder that is used to determine if we are a country poly or not.
  */
-uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys,
+uint32_t GetMultiPolyId(const std::multimap<uint32_t, Geometry>& polys,
                         const PointLL& ll,
                         GraphTileBuilder& graphtile);
 
@@ -48,7 +115,7 @@ uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys
  * @param  polys      unordered map of polys.
  * @param  ll         point that needs to be checked.
  */
-uint32_t GetMultiPolyId(const std::multimap<uint32_t, multi_polygon_type>& polys, const PointLL& ll);
+uint32_t GetMultiPolyId(const std::multimap<uint32_t, Geometry>& polys, const PointLL& ll);
 
 /**
  * Get the vector of languages for this LL.  Used by admin areas.  Checks if the pointLL is covered_by
@@ -65,30 +132,7 @@ GetMultiPolyIndexes(const language_poly_index& language_ploys, const PointLL& ll
  * @param  db           sqlite3 db handle
  * @param  aabb         bb of the tile
  */
-std::multimap<uint32_t, multi_polygon_type> GetTimeZones(Sqlite3& db, const AABB2<PointLL>& aabb);
-/**
- * Get the admin data from the spatialite db given an SQL statement
- * @param  db               sqlite3 db handle
- * @param  stmt             prepared statement object
- * @param  sql              sql commend to run.
- * @param  tilebuilder      Graph tile builder
- * @param  polys            unordered multimap of admin polys
- * @param  drive_on_right   unordered map that indicates if a country drives on right side of the
- * road
- * @param  default_languages ordered map that is used for lower admins that have an
- * default language set
- * @param  language_polys    ordered map that is used for lower admins that have an
- * default language set
- * @param  languages_only    should we only process the languages with this query
- */
-void GetData(Sqlite3& db,
-             sqlite3_stmt* stmt,
-             const std::string& sql,
-             GraphTileBuilder& tilebuilder,
-             std::multimap<uint32_t, multi_polygon_type>& polys,
-             std::unordered_map<uint32_t, bool>& drive_on_right,
-             language_poly_index& language_polys,
-             bool languages_only);
+std::multimap<uint32_t, Geometry> GetTimeZones(AdminDB& db, const AABB2<PointLL>& aabb);
 
 /**
  * Get the admin polys that intersect with the tile bounding box.
@@ -104,8 +148,8 @@ void GetData(Sqlite3& db,
  * @param  aabb              bb of the tile
  * @param  tilebuilder       Graph tile builder
  */
-std::multimap<uint32_t, multi_polygon_type>
-GetAdminInfo(Sqlite3& db,
+std::multimap<uint32_t, Geometry>
+GetAdminInfo(AdminDB& db,
              std::unordered_map<uint32_t, bool>& drive_on_right,
              std::unordered_map<uint32_t, bool>& allow_intersection_names,
              language_poly_index& language_polys,
@@ -116,7 +160,7 @@ GetAdminInfo(Sqlite3& db,
  * Get all the country access records from the db and save them to a map.
  * @param  db    sqlite3 db handle
  */
-std::unordered_map<std::string, std::vector<int>> GetCountryAccess(Sqlite3& db);
+std::unordered_map<std::string, std::vector<int>> GetCountryAccess(AdminDB& db);
 
 } // namespace mjolnir
 } // namespace valhalla
diff --git a/valhalla/mjolnir/sqlite3.h b/valhalla/mjolnir/sqlite3.h
index 100d2bc880..9f59e19e84 100644
--- a/valhalla/mjolnir/sqlite3.h
+++ b/valhalla/mjolnir/sqlite3.h
@@ -1,10 +1,10 @@
 #pragma once
 
-#include <sqlite3.h>
-
 #include <optional>
 #include <string>
 
+struct sqlite3;
+
 namespace valhalla {
 namespace mjolnir {
 
@@ -18,8 +18,7 @@ class Sqlite3 final {
   }
 
 public:
-  static std::optional<Sqlite3> open(const std::string& path,
-                                     int flags = SQLITE_OPEN_READONLY | SQLITE_OPEN_NOMUTEX);
+  static std::optional<Sqlite3> open(const std::string& path, int flags);
   ~Sqlite3();
 
   // This class cannot be copied, but can be moved
diff --git a/valhalla/mjolnir/util.h b/valhalla/mjolnir/util.h
index 50625ec5c4..5638464ea2 100644
--- a/valhalla/mjolnir/util.h
+++ b/valhalla/mjolnir/util.h
@@ -8,10 +8,6 @@
 #include <unordered_map>
 #include <vector>
 
-#include <sqlite3.h>
-// needs to be after sqlite include
-#include <spatialite.h>
-
 #include <valhalla/baldr/directededge.h>
 #include <valhalla/baldr/graphid.h>
 #include <valhalla/baldr/graphtileptr.h>


Reviews:
user: login: chrstnbwnkl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T12:20:48Z
user: login: chrstnbwnkl
body: Feel free to address that typo if you want :smile: 
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-29T12:26:16Z
user: login: kevinkreiser
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T19:23:41Z
user: login: kevinkreiser
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T19:29:35Z
user: login: chrstnbwnkl
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T20:12:20Z
user: login: kinkard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T10:46:48Z

Review comments:
diff_hunk: @@ -238,29 +261,30 @@ void GetData(sqlite3* db_handle,
         default_language = (char*)sqlite3_column_text(stmt, 8);
       }
 
-      std::string geom;
-      if (sqlite3_column_type(stmt, 9) == SQLITE_TEXT) {
-        geom = (char*)sqlite3_column_text(stmt, 9);
+      const unsigned char* wkb_blob = nullptr;
+      int wkb_size = 0;
+      if (sqlite3_column_type(stmt, 9) == SQLITE_BLOB) {
+        wkb_blob = static_cast<const unsigned char*>(sqlite3_column_blob(stmt, 9));
+        wkb_size = sqlite3_column_bytes(stmt, 9);
       }
 
       uint32_t index = tilebuilder.AddAdmin(country_name, state_name, country_iso, state_iso);
-      multi_polygon_type multi_poly;
-      bg::read_wkt(geom, multi_poly);
-      polys.emplace(index, multi_poly);
-      drive_on_right.emplace(index, dor);
-      allow_intersection_names.emplace(index, intersection_name);
 
-      bg::model::box<point_type> box{};
-      bg::envelope(multi_poly, box);
+      auto geom = db.read_wkb_and_clip(wkb_blob, wkb_size, bbox);
+
       if (!default_language.empty()) {
         auto langs = ParseLanguageTokens(default_language);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, true));
+        language_ploys.push_back(std::make_tuple(geom.clone(), langs, true));
       }
       if (!supported_languages.empty()) {
         auto langs = ParseLanguageTokens(supported_languages);
-        language_ploys.insert(std::make_tuple(box, multi_poly, langs, false));
+        language_ploys.push_back(std::make_tuple(geom.clone(), langs, false));
path: src/mjolnir/admin.cc
user: login: chrstnbwnkl
body: Old typo, feel free to ignore it though :smile: 
created_at: 2025-04-29T12:20:47Z
author_association: MEMBER
diff_hunk: @@ -14,6 +10,11 @@
 #include <boost/property_tree/ptree.hpp>
 
 #include <geos_c.h>
+#include <sqlite3.h>
+
+#include <cstdint>
+#include <string>
+#include <vector>
 
 BOOST_GEOMETRY_REGISTER_POINT_2D(valhalla::midgard::PointLL,
path: src/mjolnir/adminbuilder.cc
user: login: kevinkreiser
body: when i refactored this to use geos originally i was really hoping to get back and use geos for the whole rest of it and completely remove boost from the picture. we'll get to it one day :smile: 
created_at: 2025-04-29T19:23:41Z
author_association: MEMBER
diff_hunk: @@ -14,6 +10,11 @@
 #include <boost/property_tree/ptree.hpp>
 
 #include <geos_c.h>
+#include <sqlite3.h>
+
+#include <cstdint>
+#include <string>
+#include <vector>
 
 BOOST_GEOMETRY_REGISTER_POINT_2D(valhalla::midgard::PointLL,
path: src/mjolnir/adminbuilder.cc
user: login: kinkard
body: Oh I thought about removing `boost::geometry` from the admin builder as well, but this PR was already huge so maybe another time :)
created_at: 2025-04-30T10:46:48Z
author_association: MEMBER

PR comments:
user: login: chrstnbwnkl
created_at: 2025-04-29T12:08:16Z
body: Oops, I'm sorry about merging the other one :smile: Hope it's not too much work for you to untangle this.
author_association: MEMBER
user: login: kinkard
created_at: 2025-04-29T19:18:01Z
body: @chrstnbwnkl FYI: I've rebased this PR over master so all `Sqlite3` changes are gone from the diff.
Also I've fixed the typo =) 
author_association: MEMBER