You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sedona.apache.org by pa...@apache.org on 2026/10/20 01:11:24 UTC

(sedona-db) branch main updated: feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)

This is an automated email from the ASF dual-hosted git repository.

paleolimbot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new cce7b9112 feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)
cce7b9112 is described below

commit cce7b91124450eeb879c224b1fc2d707438f5ce9
Author: Dewey Dunnington <de...@dunnington.ca>
AuthorDate: Mon Oct 19 20:11:18 2026 -0500

    feat(r/sedonadb): configure GDAL and PROJ at runtime (#1449)
---
 Cargo.lock                                         |   1 +
 rust/sedona/src/lib.rs => c/sedona-gdal/build.rs   |  30 ++-
 c/sedona-gdal/src/dyn_load.rs                      |   3 +-
 c/sedona-gdal/src/gdal_api.rs                      | 191 ++++++++++++++++++-
 c/sedona-gdal/src/global.rs                        |  36 ++--
 python/sedonadb/python/sedonadb/context.py         | 114 ++---------
 python/sedonadb/src/lib.rs                         |  61 ++++++
 r/sedonadb/DESCRIPTION                             |   3 +-
 r/sedonadb/NAMESPACE                               |   1 +
 r/sedonadb/R/000-wrappers.R                        |  25 +++
 r/sedonadb/R/context.R                             |  83 +++++---
 r/sedonadb/R/zzz.R                                 |  18 ++
 r/sedonadb/README.Rmd                              |   4 +-
 r/sedonadb/README.md                               |  19 +-
 r/sedonadb/cleanup.win                             |   1 +
 r/sedonadb/configure.win                           | 199 +++++++++++++------
 r/sedonadb/man/sd_configure_gdal.Rd                |  21 +++
 r/sedonadb/man/sd_configure_proj.Rd                |   1 +
 r/sedonadb/src/Makevars.win.in                     |  27 ++-
 r/sedonadb/src/init.c                              |  31 +++
 r/sedonadb/{cleanup.win => src/install.libs.R}     |  12 +-
 r/sedonadb/src/rust/Cargo.toml                     |   4 +
 r/sedonadb/src/rust/api.h                          |   5 +
 r/sedonadb/src/rust/src/lib.rs                     |  64 +++++++
 r/sedonadb/tests/testthat/test-context.R           |  12 ++
 .../sedonadb-arm64-mingw-spatial-release.cmake}    |   5 +-
 .../sedonadb-mingw-static-release.cmake            |   4 +-
 .../sedonadb-x64-mingw-spatial-release.cmake}      |   5 +-
 rust/sedona/src/lib.rs                             |   1 +
 rust/sedona/src/native_library.rs                  | 210 +++++++++++++++++++++
 30 files changed, 955 insertions(+), 236 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b88d1fb19..4231e8cb5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6572,6 +6572,7 @@ dependencies = [
  "sedona-adbc",
  "sedona-expr",
  "sedona-extension",
+ "sedona-gdal",
  "sedona-geometry",
  "sedona-geoparquet",
  "sedona-proj",
diff --git a/rust/sedona/src/lib.rs b/c/sedona-gdal/build.rs
similarity index 68%
copy from rust/sedona/src/lib.rs
copy to c/sedona-gdal/build.rs
index 907848843..59442ec40 100644
--- a/rust/sedona/src/lib.rs
+++ b/c/sedona-gdal/build.rs
@@ -14,19 +14,17 @@
 // KIND, either express or implied.  See the License for the
 // specific language governing permissions and limitations
 // under the License.
-pub mod catalog;
-mod catalog_planner;
-pub mod context;
-pub mod context_builder;
-mod exec;
-pub mod memory_pool;
-mod object_storage;
-pub use object_storage::is_object_store_option;
-pub mod pool_type;
-pub mod projected_reader;
-pub mod random_geometry_provider;
-mod read;
-pub mod record_batch_reader_provider;
-pub mod show;
-pub mod size_parser;
-pub mod url_table;
+
+const GDAL_3_13_0: u32 = 3_130_000;
+
+fn main() {
+    println!("cargo:rustc-check-cfg=cfg(gdal_has_mem_create)");
+
+    if std::env::var("DEP_GDAL_VERSION_NUMBER")
+        .ok()
+        .and_then(|value| value.parse::<u32>().ok())
+        .is_some_and(|version| version >= GDAL_3_13_0)
+    {
+        println!("cargo:rustc-cfg=gdal_has_mem_create");
+    }
+}
diff --git a/c/sedona-gdal/src/dyn_load.rs b/c/sedona-gdal/src/dyn_load.rs
index 1d1cacfa1..9b5e90c90 100644
--- a/c/sedona-gdal/src/dyn_load.rs
+++ b/c/sedona-gdal/src/dyn_load.rs
@@ -264,8 +264,7 @@ fn current_process_library() -> Result<Library, GdalInitLibraryError> {
 
 #[cfg(windows)]
 fn current_process_library() -> Result<Library, GdalInitLibraryError> {
-    // Safety: loading symbols from the current process is safe.
-    Ok(unsafe { libloading::os::windows::Library::this() }
+    Ok(libloading::os::windows::Library::this()
         .map_err(|e| {
             GdalInitLibraryError::LibraryError(format!(
                 "Failed to open current process handle: {}",
diff --git a/c/sedona-gdal/src/gdal_api.rs b/c/sedona-gdal/src/gdal_api.rs
index 84f1e8695..edf8be3fa 100644
--- a/c/sedona-gdal/src/gdal_api.rs
+++ b/c/sedona-gdal/src/gdal_api.rs
@@ -24,6 +24,63 @@ use crate::dyn_load;
 use crate::errors::{GdalError, GdalInitLibraryError};
 use crate::gdal_dyn_bindgen::{GDALTransformerFunc, SedonaGdalApi};
 
+#[cfg(feature = "gdal-sys")]
+unsafe extern "C" {
+    #[link_name = "VRTCreate"]
+    fn gdal_sys_vrt_create(x_size: i32, y_size: i32) -> *mut std::ffi::c_void;
+    #[link_name = "VRTAddSimpleSource"]
+    fn gdal_sys_vrt_add_simple_source(
+        vrt_band: *mut std::ffi::c_void,
+        source_band: *mut std::ffi::c_void,
+        src_x_off: i32,
+        src_y_off: i32,
+        src_x_size: i32,
+        src_y_size: i32,
+        dst_x_off: i32,
+        dst_y_off: i32,
+        dst_x_size: i32,
+        dst_y_size: i32,
+        resampling: *const std::ffi::c_char,
+        nodata: f64,
+    ) -> i32;
+}
+
+// gdal-sys 0.12 does not expose MEMCreate, even when its bindgen feature is
+// enabled: gdal_mem.h is not included in its wrapper and MEM functions are not
+// in its allowlist. Declare the stable C ABI locally when the linked GDAL has
+// the symbol.
+#[cfg(all(feature = "gdal-sys", gdal_has_mem_create))]
+unsafe extern "C" {
+    #[link_name = "MEMCreate"]
+    fn gdal_sys_mem_create(
+        x_size: i32,
+        y_size: i32,
+        bands: i32,
+        data_type: crate::gdal_dyn_bindgen::GDALDataType,
+        options: *const *const std::ffi::c_char,
+    ) -> *mut std::ffi::c_void;
+}
+
+// GDAL 3.12 and earlier predate the public MEMCreate C API. Unix and MinGW use
+// the Itanium C++ ABI, so referencing the private constructor preserves the
+// fast path and makes static linkers retain its object from libgdal.a.
+#[cfg(all(
+    feature = "gdal-sys",
+    not(gdal_has_mem_create),
+    any(unix, all(windows, target_env = "gnu"))
+))]
+unsafe extern "C" {
+    #[link_name = "_ZN10MEMDataset6CreateEPKciii12GDALDataTypePPc"]
+    fn gdal_sys_mem_dataset_create(
+        filename: *const std::ffi::c_char,
+        x_size: i32,
+        y_size: i32,
+        bands: i32,
+        data_type: crate::gdal_dyn_bindgen::GDALDataType,
+        options: *mut *mut std::ffi::c_char,
+    ) -> *mut std::ffi::c_void;
+}
+
 /// Invoke a function pointer from the `SedonaGdalApi` struct.
 ///
 /// # Panics
@@ -48,10 +105,10 @@ pub(crate) use call_gdal_api;
 #[derive(Debug)]
 pub struct GdalApi {
     pub(crate) inner: SedonaGdalApi,
-    /// The dynamically loaded GDAL library. Kept alive for the lifetime of the
-    /// function pointers in `inner`. This is never dropped because the `GdalApi`
-    /// lives in a `static OnceLock` (see `global.rs`).
-    _lib: Library,
+    /// The dynamically loaded GDAL library, when applicable. Kept alive for the
+    /// lifetime of the function pointers in `inner`. Statically linked API
+    /// tables do not need a library handle.
+    _lib: Option<Library>,
     name: String,
 }
 
@@ -60,7 +117,7 @@ impl GdalApi {
         let (lib, inner) = dyn_load::load_gdal_from_path(&shared_library)?;
         Ok(Self {
             inner,
-            _lib: lib,
+            _lib: Some(lib),
             name: shared_library.to_string_lossy().into_owned(),
         })
     }
@@ -69,11 +126,133 @@ impl GdalApi {
         let (lib, inner) = dyn_load::load_gdal_from_current_process()?;
         Ok(Self {
             inner,
-            _lib: lib,
+            _lib: Some(lib),
             name: "current_process".to_string(),
         })
     }
 
+    /// Build the API table directly from functions linked by `gdal-sys`.
+    ///
+    /// Linked GDAL functions are not guaranteed to be exported from the final
+    /// executable or DLL for runtime symbol lookup, so retain their addresses
+    /// directly when the platform has a supported MEM creator ABI.
+    #[cfg(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu"))))]
+    pub fn from_gdal_sys() -> Self {
+        let mut inner = SedonaGdalApi::default();
+
+        macro_rules! set_fn {
+            ($name:ident) => {
+                // The generated gdal-sys bindings model C enums as integers,
+                // while Sedona's dynamic table uses repr(C) enums. Their ABIs
+                // are identical, but their Rust function-pointer types differ.
+                #[allow(clippy::missing_transmute_annotations)]
+                {
+                    inner.$name =
+                        Some(unsafe { std::mem::transmute(gdal_sys::$name as *const ()) });
+                }
+            };
+        }
+
+        set_fn!(GDALOpenEx);
+        set_fn!(GDALClose);
+        set_fn!(GDALGetRasterXSize);
+        set_fn!(GDALGetRasterYSize);
+        set_fn!(GDALGetRasterCount);
+        set_fn!(GDALGetRasterBand);
+        set_fn!(GDALGetGeoTransform);
+        set_fn!(GDALSetGeoTransform);
+        set_fn!(GDALGetProjectionRef);
+        set_fn!(GDALSetProjection);
+        set_fn!(GDALGetSpatialRef);
+        set_fn!(GDALSetSpatialRef);
+        set_fn!(GDALCreateCopy);
+        set_fn!(GDALDatasetCreateLayer);
+        set_fn!(GDALAllRegister);
+        set_fn!(GDALGetDriverByName);
+        set_fn!(GDALCreate);
+        set_fn!(GDALAddBand);
+        set_fn!(GDALRasterIO);
+        set_fn!(GDALRasterIOEx);
+        set_fn!(GDALGetRasterDataType);
+        set_fn!(GDALGetRasterBandXSize);
+        set_fn!(GDALGetRasterBandYSize);
+        set_fn!(GDALGetBlockSize);
+        set_fn!(GDALGetRasterNoDataValue);
+        set_fn!(GDALGetRasterNoDataValueAsUInt64);
+        set_fn!(GDALGetRasterNoDataValueAsInt64);
+        set_fn!(GDALSetRasterNoDataValue);
+        set_fn!(GDALDeleteRasterNoDataValue);
+        set_fn!(GDALSetRasterNoDataValueAsUInt64);
+        set_fn!(GDALSetRasterNoDataValueAsInt64);
+        set_fn!(OSRNewSpatialReference);
+        set_fn!(OSRSetFromUserInput);
+        set_fn!(OSREPSGTreatsAsLatLong);
+        set_fn!(OSRGetDataAxisToSRSAxisMapping);
+        set_fn!(OSRGetAxisMappingStrategy);
+        set_fn!(OSRSetAxisMappingStrategy);
+        set_fn!(OSRDestroySpatialReference);
+        set_fn!(OSRExportToPROJJSON);
+        set_fn!(OSRExportToWkt);
+        set_fn!(OSRClone);
+        set_fn!(OSRRelease);
+        set_fn!(OGR_G_CreateFromWkb);
+        set_fn!(OGR_G_CreateFromWkt);
+        set_fn!(OGR_G_ExportToIsoWkb);
+        set_fn!(OGR_G_WkbSize);
+        set_fn!(OGR_G_GetEnvelope);
+        set_fn!(OGR_G_DestroyGeometry);
+        set_fn!(OGR_L_ResetReading);
+        set_fn!(OGR_L_GetNextFeature);
+        set_fn!(OGR_L_CreateField);
+        set_fn!(OGR_L_GetFeatureCount);
+        set_fn!(OGR_F_GetGeometryRef);
+        set_fn!(OGR_F_GetFieldIndex);
+        set_fn!(OGR_F_GetFieldAsDouble);
+        set_fn!(OGR_F_GetFieldAsInteger);
+        set_fn!(OGR_F_IsFieldSetAndNotNull);
+        set_fn!(OGR_F_Destroy);
+        set_fn!(OGR_Fld_Create);
+        set_fn!(OGR_Fld_Destroy);
+        set_fn!(VSIFileFromMemBuffer);
+        set_fn!(VSIFCloseL);
+        set_fn!(VSIUnlink);
+        set_fn!(VSIGetMemFileBuffer);
+        set_fn!(VSIFree);
+        set_fn!(VSIMalloc);
+        inner.VRTCreate = Some(gdal_sys_vrt_create);
+        inner.VRTAddSimpleSource = Some(gdal_sys_vrt_add_simple_source);
+        set_fn!(GDALReprojectImage);
+        set_fn!(GDALCreateGenImgProjTransformer);
+        set_fn!(GDALDestroyGenImgProjTransformer);
+        set_fn!(GDALSuggestedWarpOutput);
+        set_fn!(GDALGenImgProjTransform);
+        set_fn!(GDALRasterizeGeometries);
+        set_fn!(GDALFPolygonize);
+        set_fn!(GDALPolygonize);
+        set_fn!(GDALVersionInfo);
+        set_fn!(CPLSetThreadLocalConfigOption);
+        set_fn!(CPLGetLastErrorNo);
+        set_fn!(CPLGetLastErrorMsg);
+        set_fn!(CPLErrorReset);
+        set_fn!(GDALGetDataTypeSizeBytes);
+
+        #[cfg(gdal_has_mem_create)]
+        {
+            inner.MEMCreate = Some(gdal_sys_mem_create);
+        }
+
+        #[cfg(not(gdal_has_mem_create))]
+        {
+            inner.MEMDatasetCreate = Some(gdal_sys_mem_dataset_create);
+        }
+
+        Self {
+            inner,
+            _lib: None,
+            name: "gdal_sys".to_string(),
+        }
+    }
+
     pub fn name(&self) -> &str {
         &self.name
     }
diff --git a/c/sedona-gdal/src/global.rs b/c/sedona-gdal/src/global.rs
index 3de26d7a2..2c212f1f0 100644
--- a/c/sedona-gdal/src/global.rs
+++ b/c/sedona-gdal/src/global.rs
@@ -92,28 +92,27 @@ impl GdalApiBuilder {
     /// Build a [`GdalApi`] with the configured options.
     ///
     /// When `shared_library` is set, loads GDAL from that path. Otherwise,
-    /// resolves symbols from the current process (with an optional
-    /// compile-time version check when the `gdal-sys` feature is enabled).
+    /// uses the linked `gdal-sys` function table when supported, otherwise
+    /// resolves symbols from the current process.
     pub fn build(&self) -> Result<GdalApi, GdalInitLibraryError> {
         let api = if let Some(shared_library) = &self.shared_library {
             GdalApi::try_from_shared_library(shared_library.clone())?
         } else {
-            GdalApi::try_from_current_process()?
-        };
-
-        #[cfg(feature = "gdal-sys")]
-        let get_gdal_version_info = |arg: &str| unsafe {
-            // Calling into `gdal-sys` also forces the linker to include GDAL
-            // symbols, so that `try_from_current_process` (which resolves function pointers
-            // via `dlsym` on the current process) can find them at runtime.
-            let c_arg = std::ffi::CString::new(arg).unwrap();
-            let c_version = gdal_sys::GDALVersionInfo(c_arg.as_ptr());
-            std::ffi::CStr::from_ptr(c_version)
-                .to_string_lossy()
-                .into_owned()
+            #[cfg(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu"))))]
+            {
+                GdalApi::from_gdal_sys()
+            }
+            #[cfg(not(all(feature = "gdal-sys", any(unix, all(windows, target_env = "gnu")))))]
+            {
+                GdalApi::try_from_current_process().map_err(|e| {
+                    GdalInitLibraryError::LibraryError(format!(
+                        "GDAL is not configured; call configure_gdal() or \
+                         sd_configure_gdal() before using GDAL-backed functions: {e}"
+                    ))
+                })?
+            }
         };
 
-        #[cfg(not(feature = "gdal-sys"))]
         let get_gdal_version_info = |arg: &str| api.version_info(arg);
 
         check_gdal_version(get_gdal_version_info)?;
@@ -272,12 +271,15 @@ mod test {
     use super::*;
 
     /// Building with default options (no shared library) should succeed when
-    /// `gdal-sys` is linked, loading symbols from the current process.
+    /// `gdal-sys` is linked, using its function table on supported platforms.
     #[test]
     fn test_builder_default() {
         let api = GdalApiBuilder::default()
             .build()
             .expect("GdalApiBuilder::default().build() should succeed with gdal-sys");
+        #[cfg(any(unix, all(windows, target_env = "gnu")))]
+        assert_eq!(api.name(), "gdal_sys");
+        #[cfg(not(any(unix, all(windows, target_env = "gnu"))))]
         assert_eq!(api.name(), "current_process");
     }
 
diff --git a/python/sedonadb/python/sedonadb/context.py b/python/sedonadb/python/sedonadb/context.py
index d87782781..a5c77b485 100644
--- a/python/sedonadb/python/sedonadb/context.py
+++ b/python/sedonadb/python/sedonadb/context.py
@@ -37,7 +37,11 @@ if TYPE_CHECKING:
 from sedonadb._lib import (
     InternalContext,
     configure_gdal_shared,
+    configure_gdal_prefix_native,
+    configure_gdal_system_native,
+    configure_proj_prefix_native,
     configure_proj_shared,
+    configure_proj_system_native,
     gdal_version as _gdal_version,
 )
 from sedonadb._options import Options
@@ -815,52 +819,11 @@ def _find_proj_shared_library_near_data(data_dir: Path) -> Optional[Path]:
 
 
 def _configure_proj_system():
-    # For system installs, try common library names that might be on PATH/LD_LIBRARY_PATH
-    import ctypes
-
-    names_to_try = []
-    if sys.platform == "win32":
-        names_to_try = ["proj.dll", "proj_9.dll"]
-    elif sys.platform == "darwin":
-        names_to_try = ["libproj.dylib"]
-    else:
-        names_to_try = ["libproj.so"]
-
-    for name in names_to_try:
-        try:
-            ctypes.CDLL(name)
-            configure_proj(shared_library=name)
-            return
-        except OSError:
-            continue
-
-    raise ValueError(
-        f"Can't load PROJ shared library '{names_to_try[0]}': "
-        "Could not find module (or one of its dependencies). "
-        "Try using the full path with constructor syntax."
-    )
+    configure_proj_system_native()
 
 
 def _configure_proj_prefix(prefix: str):
-    prefix = Path(prefix)
-    if not prefix.exists():
-        raise ValueError(f"Can't configure PROJ from prefix '{prefix}': does not exist")
-
-    if sys.platform == "win32":
-        # Windows conda uses Library/bin for DLLs and Library/share for data
-        lib_dir = prefix / "Library" / "bin"
-        data_dir = prefix / "Library" / "share" / "proj"
-    else:
-        lib_dir = prefix / "lib"
-        data_dir = prefix / "share" / "proj"
-
-    shared_library = _find_proj_shared_library(lib_dir)
-
-    configure_proj(
-        shared_library=shared_library,
-        database_path=data_dir / "proj.db",
-        search_path=data_dir,
-    )
+    configure_proj_prefix_native(str(prefix))
 
 
 def configure_gdal(
@@ -917,9 +880,11 @@ def configure_gdal(
             return
         elif preset == "homebrew":
             prefix = os.environ.get("HOMEBREW_PREFIX", "/opt/homebrew")
-            shared_library = Path(prefix) / "lib" / _gdal_lib_name()
+            configure_gdal_prefix_native(prefix)
+            return
         elif preset == "system":
-            shared_library = _gdal_lib_name()
+            configure_gdal_system_native()
+            return
         elif preset == "auto":
             # The GDAL library bundled with rasterio has more features enabled by default
             # (e.g., more compression codecs) than the one bundled with pyogrio, so try
@@ -965,15 +930,6 @@ def configure_gdal(
     configure_gdal_shared(str(shared_library))
 
 
-def _gdal_lib_name() -> str:
-    if sys.platform == "win32":
-        return "gdal.dll"
-    elif sys.platform == "darwin":
-        return "libgdal.dylib"
-    else:
-        return "libgdal.so"
-
-
 def _find_gdal_in_package(pkg_name: str) -> Path:
     """Locate the bundled GDAL shared library inside a pip-installed package.
 
@@ -1027,42 +983,6 @@ def _configure_gdal_rasterio():
     configure_gdal(shared_library=_find_gdal_in_package("rasterio"))
 
 
-def _gdal_lib_pattern() -> str:
-    """Return a glob pattern for finding GDAL shared libraries."""
-    if sys.platform == "win32":
-        return "gdal*.dll"
-    elif sys.platform == "darwin":
-        return "libgdal*.dylib"
-    else:
-        return "libgdal.so*"
-
-
-def _find_gdal_shared_library(lib_dir: Path) -> Path:
-    """Find the GDAL shared library in the given directory.
-
-    Raises ValueError if not found or multiple matches.
-    """
-    pattern = _gdal_lib_pattern()
-    possible_files = list(lib_dir.glob(pattern))
-
-    # Filter out debug/test variants if multiple matches
-    if len(possible_files) > 1:
-        # Prefer exact matches or versioned libraries without suffixes like _d
-        filtered = [f for f in possible_files if "_d." not in f.name]
-        if filtered:
-            possible_files = filtered
-
-    if len(possible_files) == 0:
-        raise ValueError(
-            f"Can't find GDAL shared library matching '{pattern}' in '{lib_dir}'"
-        )
-    if len(possible_files) > 1:
-        # Pick the one with shortest name (usually the main library)
-        possible_files.sort(key=lambda p: len(p.name))
-
-    return possible_files[0]
-
-
 def _configure_gdal_conda():
     # Try CONDA_PREFIX first, then fall back to sys.prefix
     conda_prefix = os.environ.get("CONDA_PREFIX")
@@ -1073,19 +993,7 @@ def _configure_gdal_conda():
         # may not be set, but sys.prefix points to the environment
         prefix = Path(sys.prefix)
 
-    if not prefix.exists():
-        raise ValueError(f"Can't configure GDAL from prefix '{prefix}': does not exist")
-
-    if sys.platform == "win32":
-        lib_dir = prefix / "Library" / "bin"
-    else:
-        lib_dir = prefix / "lib"
-
-    if not lib_dir.exists():
-        raise ValueError(f"Can't find GDAL library directory '{lib_dir}'")
-
-    shared_library = _find_gdal_shared_library(lib_dir)
-    configure_gdal(shared_library=shared_library)
+    configure_gdal_prefix_native(str(prefix))
 
 
 def gdal_version() -> Optional[str]:
diff --git a/python/sedonadb/src/lib.rs b/python/sedonadb/src/lib.rs
index 0a49edbe9..5d5946ff7 100644
--- a/python/sedonadb/src/lib.rs
+++ b/python/sedonadb/src/lib.rs
@@ -21,6 +21,7 @@ use crate::{
     udf::{sedona_aggregate_udf, sedona_native_scalar_udf, sedona_scalar_udf},
 };
 use pyo3::{exceptions::PyValueError, ffi::Py_uintptr_t, prelude::*};
+use sedona::native_library::NativeLibrary;
 use sedona_adbc::AdbcSedonadbDriverInit;
 use sedona_gdal::global::{configure_global_gdal_api, with_global_gdal, GdalApiBuilder};
 use sedona_proj::register::{configure_global_proj_engine, ProjCrsEngineBuilder};
@@ -98,6 +99,9 @@ fn configure_proj_shared(
         builder = builder.with_search_paths(vec![search_path.into()]);
     }
 
+    builder
+        .build()
+        .map_err(|e| PySedonaError::SedonaPython(e.to_string()))?;
     configure_global_proj_engine(builder)
         .map_err(|e| PySedonaError::SedonaPython(e.to_string()))?;
     Ok(())
@@ -106,12 +110,65 @@ fn configure_proj_shared(
 #[pyfunction]
 fn configure_gdal_shared(shared_library_path: String) -> Result<(), PySedonaError> {
     let builder = GdalApiBuilder::default().with_shared_library(shared_library_path.into());
+    builder.build().map_err(|e| {
+        PySedonaError::SedonaPython(format!("Failed to load GDAL shared library: {e}"))
+    })?;
     configure_global_gdal_api(builder).map_err(|e| {
         PySedonaError::SedonaPython(format!("Failed to configure GDAL shared library: {e}"))
     })?;
     Ok(())
 }
 
+#[pyfunction]
+fn configure_proj_prefix_native(prefix: String) -> Result<(), PySedonaError> {
+    let installation = NativeLibrary::Proj
+        .from_prefix(prefix.as_ref())
+        .map_err(PySedonaError::SedonaPython)?;
+    configure_proj_shared(
+        Some(installation.shared_library.to_string_lossy().into_owned()),
+        Some(
+            installation
+                .data_dir
+                .join("proj.db")
+                .to_string_lossy()
+                .into_owned(),
+        ),
+        Some(installation.data_dir.to_string_lossy().into_owned()),
+    )
+}
+
+#[pyfunction]
+fn configure_proj_system_native() -> Result<(), PySedonaError> {
+    let mut errors = Vec::new();
+    for candidate in NativeLibrary::Proj.system_candidates() {
+        match configure_proj_shared(Some(candidate.to_string_lossy().into_owned()), None, None) {
+            Ok(()) => return Ok(()),
+            Err(e) => errors.push(e.to_string()),
+        }
+    }
+    Err(PySedonaError::SedonaPython(errors.join("; ")))
+}
+
+#[pyfunction]
+fn configure_gdal_prefix_native(prefix: String) -> Result<(), PySedonaError> {
+    let installation = NativeLibrary::Gdal
+        .from_prefix(prefix.as_ref())
+        .map_err(PySedonaError::SedonaPython)?;
+    configure_gdal_shared(installation.shared_library.to_string_lossy().into_owned())
+}
+
+#[pyfunction]
+fn configure_gdal_system_native() -> Result<(), PySedonaError> {
+    let mut errors = Vec::new();
+    for candidate in NativeLibrary::Gdal.system_candidates() {
+        match configure_gdal_shared(candidate.to_string_lossy().into_owned()) {
+            Ok(()) => return Ok(()),
+            Err(e) => errors.push(e.to_string()),
+        }
+    }
+    Err(PySedonaError::SedonaPython(errors.join("; ")))
+}
+
 /// Derive a north-up, GDAL-order geotransform from a spatial bounding box and
 /// grid shape. Thin wrapper over `geotransform_from_bbox_and_spatial_shape` so
 /// the bbox-to-transform math stays a single Rust source of truth.
@@ -156,7 +213,11 @@ fn _lib(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
 
     m.add_function(wrap_pyfunction!(begin_gdal_shutdown, m)?)?;
     m.add_function(wrap_pyfunction!(configure_gdal_shared, m)?)?;
+    m.add_function(wrap_pyfunction!(configure_gdal_prefix_native, m)?)?;
+    m.add_function(wrap_pyfunction!(configure_gdal_system_native, m)?)?;
     m.add_function(wrap_pyfunction!(configure_proj_shared, m)?)?;
+    m.add_function(wrap_pyfunction!(configure_proj_prefix_native, m)?)?;
+    m.add_function(wrap_pyfunction!(configure_proj_system_native, m)?)?;
     m.add_function(wrap_pyfunction!(expr::expr_binary, m)?)?;
     m.add_function(wrap_pyfunction!(expr::expr_col, m)?)?;
     m.add_function(wrap_pyfunction!(expr::expr_lit, m)?)?;
diff --git a/r/sedonadb/DESCRIPTION b/r/sedonadb/DESCRIPTION
index d011449e9..50331dd37 100644
--- a/r/sedonadb/DESCRIPTION
+++ b/r/sedonadb/DESCRIPTION
@@ -8,7 +8,8 @@ Description: Provides bindings for Apache SedonaDB, a lightweight
 License: Apache License (>= 2)
 Encoding: UTF-8
 Roxygen: list(markdown = TRUE)
-SystemRequirements: Cargo (Rust's package manager), rustc, CMake, Git
+SystemRequirements: Cargo (Rust's package manager), rustc, CMake, Git, curl,
+    pkg-config, GEOS, GDAL (>= 3.5), PROJ (>= 6.2)
 Depends: R (>= 4.1.0)
 Suggests:
     adbcdrivermanager,
diff --git a/r/sedonadb/NAMESPACE b/r/sedonadb/NAMESPACE
index c638a9cf8..45d955002 100644
--- a/r/sedonadb/NAMESPACE
+++ b/r/sedonadb/NAMESPACE
@@ -53,6 +53,7 @@ export(sd_anti_join)
 export(sd_arrange)
 export(sd_collect)
 export(sd_compute)
+export(sd_configure_gdal)
 export(sd_configure_proj)
 export(sd_connect)
 export(sd_count)
diff --git a/r/sedonadb/R/000-wrappers.R b/r/sedonadb/R/000-wrappers.R
index a1f99acd4..2d86a1535 100644
--- a/r/sedonadb/R/000-wrappers.R
+++ b/r/sedonadb/R/000-wrappers.R
@@ -85,6 +85,31 @@ NULL
 }
 
 
+`configure_gdal_prefix_native` <- function(`prefix`) {
+  invisible(.Call(savvy_configure_gdal_prefix_native__impl, `prefix`))
+}
+
+
+`configure_gdal_shared` <- function(`shared_library_path` = NULL) {
+  invisible(.Call(savvy_configure_gdal_shared__impl, `shared_library_path`))
+}
+
+
+`configure_gdal_system_native` <- function() {
+  invisible(.Call(savvy_configure_gdal_system_native__impl))
+}
+
+
+`configure_proj_prefix_native` <- function(`prefix`) {
+  invisible(.Call(savvy_configure_proj_prefix_native__impl, `prefix`))
+}
+
+
+`configure_proj_system_native` <- function() {
+  invisible(.Call(savvy_configure_proj_system_native__impl))
+}
+
+
 `init_r_runtime_interrupts` <- function(`interrupts_call`, `pkg_env`) {
   invisible(.Call(savvy_init_r_runtime_interrupts__impl, `interrupts_call`, `pkg_env`))
 }
diff --git a/r/sedonadb/R/context.R b/r/sedonadb/R/context.R
index 9fea789ab..701e5fd2e 100644
--- a/r/sedonadb/R/context.R
+++ b/r/sedonadb/R/context.R
@@ -429,6 +429,7 @@ global_ctx$ctx <- NULL
 #' not linked at build time.
 #'
 #' @param preset One of:
+#'   - `"conda"`: Look for PROJ in `CONDA_PREFIX`.
 #'   - `"homebrew"`: Look for PROJ installed by Homebrew. This is the easiest
 #'     option on MacOS.
 #'   - `"system"`: Look for PROJ in the platform library load path (e.g.,
@@ -456,6 +457,10 @@ sd_configure_proj <- function(
   if (!is.null(preset)) {
     switch(
       preset,
+      conda = {
+        configure_proj_prefix(Sys.getenv("CONDA_PREFIX"))
+        return(invisible(NULL))
+      },
       homebrew = {
         configure_proj_prefix(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
         return(invisible(NULL))
@@ -465,7 +470,7 @@ sd_configure_proj <- function(
         return(invisible(NULL))
       },
       auto = {
-        presets <- c("homebrew", "system")
+        presets <- c("conda", "homebrew", "system")
         errors <- c()
         for (preset in presets) {
           maybe_err <- try(sd_configure_proj(preset), silent = TRUE)
@@ -513,30 +518,66 @@ sd_configure_proj <- function(
 }
 
 configure_proj_system <- function() {
-  sd_configure_proj(shared_library = proj_dll_name())
+  configure_proj_system_native()
 }
 
 configure_proj_prefix <- function(prefix) {
-  if (!dir.exists(prefix)) {
-    stop(sprintf("Can't configure PROJ from prefix '%s': does not exist", prefix))
+  configure_proj_prefix_native(prefix)
+}
+
+#' Configure GDAL
+#'
+#' SedonaDB loads GDAL dynamically at runtime. Windows binary packages bundle
+#' the GDAL DLL and matching data files built by vcpkg. Automatic configuration
+#' is attempted when the package is loaded.
+#'
+#' @param preset One of `"conda"`, `"homebrew"`, `"system"`, or `"auto"`.
+#' @param shared_library A path or platform library name for GDAL.
+#'
+#' @returns NULL, invisibly.
+#' @export
+sd_configure_gdal <- function(preset = NULL, shared_library = NULL) {
+  if (!is.null(preset)) {
+    switch(
+      preset,
+      conda = {
+        configure_gdal_prefix_native(Sys.getenv("CONDA_PREFIX"))
+        return(invisible(NULL))
+      },
+      homebrew = {
+        configure_gdal_prefix_native(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
+        return(invisible(NULL))
+      },
+      system = {
+        configure_gdal_system_native()
+        return(invisible(NULL))
+      },
+      auto = {
+        presets <- c("conda", "homebrew", "system")
+        errors <- character()
+        for (candidate in presets) {
+          maybe_err <- try(sd_configure_gdal(candidate), silent = TRUE)
+          if (!inherits(maybe_err, "try-error")) {
+            return(invisible(NULL))
+          }
+          errors <- c(errors, sprintf("%s: %s", candidate, maybe_err))
+        }
+
+        packageStartupMessage(sprintf(
+          "Failed to configure GDAL (tried %s):\n%s",
+          paste0("'", presets, "'", collapse = ", "),
+          paste0(errors, collapse = "\n")
+        ))
+        return(invisible(NULL))
+      },
+      stop(sprintf("Unknown preset: '%s'", preset))
+    )
   }
 
-  sd_configure_proj(
-    shared_library = file.path(prefix, "lib", proj_dll_name()),
-    database_path = file.path(prefix, "share", "proj", "proj.db"),
-    search_path = file.path(prefix, "share", "proj")
-  )
-}
+  if (is.null(shared_library)) {
+    stop("Must provide `shared_library` or `preset`")
+  }
 
-proj_dll_name <- function() {
-  switch(
-    tolower(Sys.info()[["sysname"]]),
-    windows = "proj.dll",
-    darwin = "libproj.dylib",
-    linux = "libproj.so",
-    stop(sprintf(
-      "Can't determine system PROJ shared library name for OS: %s",
-      Sys.info()[["sysname"]]
-    ))
-  )
+  configure_gdal_shared(shared_library)
+  invisible(NULL)
 }
diff --git a/r/sedonadb/R/zzz.R b/r/sedonadb/R/zzz.R
index 0eb13202c..82f23051b 100644
--- a/r/sedonadb/R/zzz.R
+++ b/r/sedonadb/R/zzz.R
@@ -25,6 +25,24 @@
   ns <- asNamespace("sedonadb")
   call <- call("check_interrupts")
   init_r_runtime_interrupts(call, ns)
+
+  bundled_proj <- system.file("proj", package = "sedonadb")
+  if (nzchar(bundled_proj)) {
+    configure_proj_shared(
+      database_path = file.path(bundled_proj, "proj.db"),
+      search_path = bundled_proj
+    )
+  } else {
+    sd_configure_proj("auto")
+  }
+
+  bundled_gdal <- system.file("gdal", package = "sedonadb")
+  if (nzchar(bundled_gdal)) {
+    Sys.setenv(GDAL_DATA = bundled_gdal)
+    configure_gdal_shared()
+  } else {
+    sd_configure_gdal("auto")
+  }
 }
 
 # The function we call from Rust to check for interrupts. R checks for
diff --git a/r/sedonadb/README.Rmd b/r/sedonadb/README.Rmd
index fc3562c9a..dfbf6d046 100644
--- a/r/sedonadb/README.Rmd
+++ b/r/sedonadb/README.Rmd
@@ -53,7 +53,9 @@ You can install the development version of sedonadb from [GitHub](https://github
 pak::pkg_install("apache/sedona-db/r/sedonadb")
 ```
 
-Installing a development version of sedonadb requires a [Rust compiler](https://rustup.rs), CMake, Git, and a GEOS system dependency (e.g., `brew install geos` or `apt-get install libgeos-dev`). Install instructions for GEOS on other platforms can be found on the [sf package homepage](https://r-spatial.github.io/sf). The install automatically sets up a pinned vcpkg checkout for the S2, Abseil, and OpenSSL dependencies. Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and use dep [...]
+Installing a development version of sedonadb requires a [Rust compiler](https://rustup.rs), CMake, Git, pkg-config, curl, and GEOS. GDAL and PROJ shared libraries are also required to use raster and coordinate transformation functions. For example, on Ubuntu use `apt-get install libgeos-dev libgdal-dev libproj-dev pkg-config curl`, or on macOS use `brew install geos gdal proj pkg-config`.
+
+The Windows build automatically sets up a pinned vcpkg checkout and builds and statically links GDAL, PROJ, S2, Abseil, and OpenSSL with the Rtools compiler. The matching GDAL/PROJ data files are bundled in the installed package. Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and provide a bootstrapped checkout through `VCPKG_ROOT` instead.
 
 ## Example
 
diff --git a/r/sedonadb/README.md b/r/sedonadb/README.md
index 5bdd7f7a3..7005d6aba 100644
--- a/r/sedonadb/README.md
+++ b/r/sedonadb/README.md
@@ -47,13 +47,18 @@ pak::pkg_install("apache/sedona-db/r/sedonadb")
 ```
 
 Installing a development version of sedonadb requires a [Rust
-compiler](https://rustup.rs), CMake, Git, and a GEOS system dependency
-(e.g., `brew install geos` or `apt-get install libgeos-dev`). Install
-instructions for GEOS on other platforms can be found on the [sf package
-homepage](https://r-spatial.github.io/sf). The install automatically sets
-up a pinned vcpkg checkout for the S2, Abseil, and OpenSSL dependencies.
-Set `SEDONADB_USE_VCPKG=false` to disable automatic setup and use
-dependencies provided through `VCPKG_ROOT` or the system instead.
+compiler](https://rustup.rs), CMake, Git, pkg-config, curl, and GEOS. GDAL
+and PROJ shared libraries are also required to use raster and coordinate
+transformation functions. For example, on Ubuntu use `apt-get install
+libgeos-dev libgdal-dev libproj-dev pkg-config curl`, or on macOS use
+`brew install geos gdal proj pkg-config`.
+
+The Windows build automatically sets up a pinned vcpkg checkout and
+builds and statically links GDAL, PROJ, S2, Abseil, and OpenSSL with the
+Rtools compiler. The matching GDAL/PROJ data files are bundled in the
+installed package. Set
+`SEDONADB_USE_VCPKG=false` to disable automatic setup and provide a
+bootstrapped checkout through `VCPKG_ROOT` instead.
 
 ## Example
 
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/cleanup.win
index e81e481cb..5a9d2a1c0 100755
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/cleanup.win
@@ -16,3 +16,4 @@
 # under the License.
 
 rm -f src/Makevars.win
+rm -rf inst/gdal inst/proj
diff --git a/r/sedonadb/configure.win b/r/sedonadb/configure.win
index 090987185..cd865aa37 100755
--- a/r/sedonadb/configure.win
+++ b/r/sedonadb/configure.win
@@ -20,7 +20,7 @@
 # available (even if it isn't used). Here we just try pkg-config (LIB_DIR
 # can be used to specify the location of -lgeos_c)
 
-pkg-config geos  2>/dev/null
+pkg-config geos 2>/dev/null
 if [ $? -eq 0 ]; then
   PKGCONFIG_LIBS=`pkg-config --libs geos`
 fi
@@ -105,9 +105,13 @@ fi
 # Build spherical geography support by default. Setting SAVVY_FEATURES keeps
 # its existing role as an explicit feature override.
 if [ -z "${SAVVY_FEATURES+x}" ]; then
-  SAVVY_FEATURES="s2geography"
+  SAVVY_FEATURES="s2geography gdal proj"
 fi
 
+# Cargo accepts comma- or space-separated feature lists. Normalize to spaces so
+# the feature checks below handle both forms consistently.
+SAVVY_FEATURES="$(printf '%s' "${SAVVY_FEATURES}" | tr ',' ' ')"
+
 # e.g. SAVVY_FEATURES="a b"  -->  "--features 'a b'"
 if [ -n "${SAVVY_FEATURES}" ]; then
   FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
@@ -127,70 +131,151 @@ case "$($CC -dumpmachine)" in
 esac
 echo "using Rust target: '${TARGET}'"
 
-# sedona-s2geography needs OpenSSL, Abseil, and s2geometry built with the same
-# MinGW/gnullvm compiler and flags as R. The usual vcpkg Windows triplets use
-# MSVC, whose C++ ABI is not compatible with Rtools.
+# gdal-sys ships pre-generated Windows bindings for x86_64 but not arm64.
+# Generate them from the vcpkg headers for the gnullvm target.
+case "${TARGET}" in
+  aarch64-*)
+    case " ${SAVVY_FEATURES} " in
+      *" gdal "*)
+        SAVVY_FEATURES="${SAVVY_FEATURES} bindgen"
+        FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
+        ;;
+    esac
+    ;;
+esac
+
+# Native C/C++ dependencies must use the same MinGW/gnullvm compiler and flags
+# as R. The usual vcpkg Windows triplets use MSVC, whose C++ ABI is not
+# compatible with Rtools.
 S2_VCPKG_ROOT=""
 S2_VCPKG_TARGET_TRIPLET=""
 S2_VCPKG_OVERLAY_TRIPLETS=""
 S2_CMAKE_TOOLCHAIN_FILE=""
 S2_CMAKE_PREFIX_PATH=""
 S2_OPENSSL_ROOT_DIR=""
-SAVVY_FEATURE_WORDS="$(printf '%s' "${SAVVY_FEATURES}" | tr ',' ' ')"
-case " ${SAVVY_FEATURE_WORDS} " in
-  *" s2geography "*)
-    case "${SEDONADB_USE_VCPKG:-true}" in
-      0|false|FALSE|no|NO|off|OFF) SEDONADB_SETUP_VCPKG=false ;;
-      *) SEDONADB_SETUP_VCPKG=true ;;
-    esac
+SPATIAL_VCPKG_PREFIX=""
+GDAL_VERSION=""
 
-    if [ "${SEDONADB_SETUP_VCPKG}" = true ] && [ -z "${VCPKG_ROOT}" ]; then
-      VCPKG_ROOT="$(sh tools/setup-vcpkg.sh)" || exit 1
-    fi
-
-    if [ -z "${VCPKG_ROOT}" ] || [ ! -x "${VCPKG_ROOT}/vcpkg.exe" ]; then
-      echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
-      echo ""
-      echo "Building with SAVVY_FEATURES=s2geography on Windows requires"
-      echo "VCPKG_ROOT to point to a bootstrapped vcpkg checkout, or"
-      echo "automatic setup via SEDONADB_USE_VCPKG (the default)."
-      echo ""
-      echo "---------------------------------------------------------------"
-      exit 1
-    fi
-
-    VCPKG_ROOT="$(cygpath -m "${VCPKG_ROOT}")"
-    CXX="$("${R_HOME}/bin/R" CMD config CXX17)"
-    CPPFLAGS="$("${R_HOME}/bin/R" CMD config CPPFLAGS)"
-    CFLAGS="$("${R_HOME}/bin/R" CMD config CFLAGS)"
-    CXXFLAGS="$("${R_HOME}/bin/R" CMD config CXX17FLAGS)"
-    LDFLAGS="$("${R_HOME}/bin/R" CMD config LDFLAGS)"
-
-    case "${TARGET}" in
-      aarch64-*) S2_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-static-release" ;;
-      *) S2_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-static-release" ;;
-    esac
+NEEDS_S2=false
+NEEDS_GDAL=false
+NEEDS_PROJ=false
+case " ${SAVVY_FEATURES} " in
+  *" s2geography "*) NEEDS_S2=true ;;
+esac
+case " ${SAVVY_FEATURES} " in
+  *" gdal "*) NEEDS_GDAL=true ;;
+esac
+case " ${SAVVY_FEATURES} " in
+  *" proj "*) NEEDS_PROJ=true ;;
+esac
+
+if [ "${NEEDS_S2}" = true ] || [ "${NEEDS_GDAL}" = true ] || [ "${NEEDS_PROJ}" = true ]; then
+  case "${SEDONADB_USE_VCPKG:-true}" in
+    0|false|FALSE|no|NO|off|OFF) SEDONADB_SETUP_VCPKG=false ;;
+    *) SEDONADB_SETUP_VCPKG=true ;;
+  esac
+
+  if [ "${SEDONADB_SETUP_VCPKG}" = true ] && [ -z "${VCPKG_ROOT}" ]; then
+    VCPKG_ROOT="$(sh tools/setup-vcpkg.sh)" || exit 1
+  fi
+
+  if [ -z "${VCPKG_ROOT}" ] || [ ! -x "${VCPKG_ROOT}/vcpkg.exe" ]; then
+    echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
+    echo ""
+    echo "Building the requested native features on Windows requires"
+    echo "VCPKG_ROOT to point to a bootstrapped vcpkg checkout, or"
+    echo "automatic setup via SEDONADB_USE_VCPKG (the default)."
+    echo ""
+    echo "---------------------------------------------------------------"
+    exit 1
+  fi
+
+  VCPKG_ROOT="$(cygpath -m "${VCPKG_ROOT}")"
+  VCPKG_OVERLAY_TRIPLETS="$(pwd)/tools/vcpkg-triplets"
+  CXX="$("${R_HOME}/bin/R" CMD config CXX17)"
+  CPPFLAGS="$("${R_HOME}/bin/R" CMD config CPPFLAGS)"
+  CFLAGS="$("${R_HOME}/bin/R" CMD config CFLAGS)"
+  CXXFLAGS="$("${R_HOME}/bin/R" CMD config CXX17FLAGS)"
+  LDFLAGS="$("${R_HOME}/bin/R" CMD config LDFLAGS)"
+
+  export SEDONADB_R_CC="${CC}"
+  export SEDONADB_R_CXX="${CXX}"
+  export SEDONADB_R_CFLAGS="${CPPFLAGS} ${CFLAGS}"
+  export SEDONADB_R_CXXFLAGS="${CPPFLAGS} ${CXXFLAGS}"
+  export SEDONADB_R_LDFLAGS="${LDFLAGS}"
+fi
+
+if [ "${NEEDS_S2}" = true ]; then
+  case "${TARGET}" in
+    aarch64-*) S2_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-static-release" ;;
+    *) S2_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-static-release" ;;
+  esac
+
+  S2_VCPKG_ROOT="${VCPKG_ROOT}"
+  echo "building s2geography dependencies with R's compiler (${CC}, ${CXX})"
+  "${VCPKG_ROOT}/vcpkg.exe" \
+    --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+    --host-triplet=x64-mingw-static \
+    --triplet="${S2_VCPKG_TARGET_TRIPLET}" \
+    install openssl abseil s2geometry || exit 1
+
+  S2_VCPKG_OVERLAY_TRIPLETS="$(cygpath -m "${VCPKG_OVERLAY_TRIPLETS}")"
+  S2_CMAKE_TOOLCHAIN_FILE="${S2_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
+  S2_CMAKE_PREFIX_PATH="${S2_VCPKG_ROOT}/installed/${S2_VCPKG_TARGET_TRIPLET}"
+  S2_OPENSSL_ROOT_DIR="${S2_CMAKE_PREFIX_PATH}"
+fi
 
-    S2_VCPKG_ROOT="${VCPKG_ROOT}"
-    S2_VCPKG_OVERLAY_TRIPLETS="$(pwd)/tools/vcpkg-triplets"
-    export SEDONADB_R_CC="${CC}"
-    export SEDONADB_R_CXX="${CXX}"
-    export SEDONADB_R_CFLAGS="${CPPFLAGS} ${CFLAGS}"
-    export SEDONADB_R_CXXFLAGS="${CPPFLAGS} ${CXXFLAGS}"
-    export SEDONADB_R_LDFLAGS="${LDFLAGS}"
+if [ "${NEEDS_GDAL}" = true ] || [ "${NEEDS_PROJ}" = true ]; then
+  case "${TARGET}" in
+    aarch64-*) SPATIAL_VCPKG_TARGET_TRIPLET="sedonadb-arm64-mingw-spatial-release" ;;
+    *) SPATIAL_VCPKG_TARGET_TRIPLET="sedonadb-x64-mingw-spatial-release" ;;
+  esac
 
-    echo "building s2geography dependencies with R's compiler (${CC}, ${CXX})"
+  if [ "${NEEDS_GDAL}" = true ]; then
+    echo "building GDAL and PROJ with R's compiler (${CC}, ${CXX})"
     "${VCPKG_ROOT}/vcpkg.exe" \
-      --overlay-triplets="${S2_VCPKG_OVERLAY_TRIPLETS}" \
-      --triplet="${S2_VCPKG_TARGET_TRIPLET}" \
-      install openssl abseil s2geometry || exit 1
-
-    S2_VCPKG_OVERLAY_TRIPLETS="$(cygpath -m "${S2_VCPKG_OVERLAY_TRIPLETS}")"
-    S2_CMAKE_TOOLCHAIN_FILE="${S2_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
-    S2_CMAKE_PREFIX_PATH="${S2_VCPKG_ROOT}/installed/${S2_VCPKG_TARGET_TRIPLET}"
-    S2_OPENSSL_ROOT_DIR="${S2_CMAKE_PREFIX_PATH}"
-    ;;
-esac
+      --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+      --host-triplet=x64-mingw-static \
+      --triplet="${SPATIAL_VCPKG_TARGET_TRIPLET}" \
+      install "gdal[core]" || exit 1
+  else
+    echo "building PROJ with R's compiler (${CC}, ${CXX})"
+    "${VCPKG_ROOT}/vcpkg.exe" \
+      --overlay-triplets="${VCPKG_OVERLAY_TRIPLETS}" \
+      --host-triplet=x64-mingw-static \
+      --triplet="${SPATIAL_VCPKG_TARGET_TRIPLET}" \
+      install proj || exit 1
+  fi
+
+  SPATIAL_VCPKG_PREFIX="${VCPKG_ROOT}/installed/${SPATIAL_VCPKG_TARGET_TRIPLET}"
+  SPATIAL_PKG_CONFIG_PREFIX="$(cygpath -u "${SPATIAL_VCPKG_PREFIX}")"
+  SPATIAL_PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_PREFIX}/lib/pkgconfig:${SPATIAL_PKG_CONFIG_PREFIX}/share/pkgconfig"
+  if [ "${NEEDS_GDAL}" = true ]; then
+    GDAL_VERSION="$(awk '
+      /#define GDAL_VERSION_MAJOR/ { major=$3 }
+      /#define GDAL_VERSION_MINOR/ { minor=$3 }
+      /#define GDAL_VERSION_REV/ { patch=$3 }
+      END { print major "." minor "." patch }
+    ' "${SPATIAL_VCPKG_PREFIX}/include/gdal_version.h")"
+    SPATIAL_LIBS_RAW="$(PKG_CONFIG_PATH= PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_LIBDIR}" PKG_CONFIG_SYSROOT_DIR= pkg-config --static --libs gdal)" || exit 1
+
+    # gdal-sys bundles libgdal.a into libsedonadbr.a, but a Rust staticlib
+    # does not propagate GDAL's transitive native link requirements to R.
+    # Keep the vcpkg dependency closure while removing the duplicate archive.
+    SPATIAL_LIBS=""
+    for SPATIAL_LIB in ${SPATIAL_LIBS_RAW}; do
+      if [ "${SPATIAL_LIB}" != "-lgdal" ]; then
+        SPATIAL_LIBS="${SPATIAL_LIBS} ${SPATIAL_LIB}"
+      fi
+    done
+  else
+    SPATIAL_LIBS="$(PKG_CONFIG_PATH= PKG_CONFIG_LIBDIR="${SPATIAL_PKG_CONFIG_LIBDIR}" PKG_CONFIG_SYSROOT_DIR= pkg-config --static --libs proj)" || exit 1
+  fi
+
+  # Rescan the static dependency closure because GDAL and PROJ contain cycles.
+  # The package-level GEOS dependency remains separate and follows the group.
+  PKG_LIBS="-Wl,--start-group ${SPATIAL_LIBS} -Wl,--end-group ${PKGCONFIG_LIBS:--lgeos_c -lgeos}"
+fi
 
 sed \
   -e "s/@TARGET@/${TARGET}/" \
@@ -203,5 +288,7 @@ sed \
   -e "s|@CMAKE_TOOLCHAIN_FILE@|${S2_CMAKE_TOOLCHAIN_FILE}|" \
   -e "s|@CMAKE_PREFIX_PATH@|${S2_CMAKE_PREFIX_PATH}|" \
   -e "s|@OPENSSL_ROOT_DIR@|${S2_OPENSSL_ROOT_DIR}|" \
+  -e "s|@SPATIAL_VCPKG_PREFIX@|${SPATIAL_VCPKG_PREFIX}|" \
+  -e "s|@GDAL_VERSION@|${GDAL_VERSION}|" \
   -e "s|@PKG_LIBS@|${PKG_LIBS}|" \
   src/Makevars.win.in > src/Makevars.win
diff --git a/r/sedonadb/man/sd_configure_gdal.Rd b/r/sedonadb/man/sd_configure_gdal.Rd
new file mode 100644
index 000000000..b4ad9269e
--- /dev/null
+++ b/r/sedonadb/man/sd_configure_gdal.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_configure_gdal}
+\alias{sd_configure_gdal}
+\title{Configure GDAL}
+\usage{
+sd_configure_gdal(preset = NULL, shared_library = NULL)
+}
+\arguments{
+\item{preset}{One of \code{"conda"}, \code{"homebrew"}, \code{"system"}, or \code{"auto"}.}
+
+\item{shared_library}{A path or platform library name for GDAL.}
+}
+\value{
+NULL, invisibly.
+}
+\description{
+SedonaDB loads GDAL dynamically at runtime. Windows binary packages bundle
+the GDAL DLL and matching data files built by vcpkg. Automatic configuration
+is attempted when the package is loaded.
+}
diff --git a/r/sedonadb/man/sd_configure_proj.Rd b/r/sedonadb/man/sd_configure_proj.Rd
index 2ddb2b179..cf92eecdc 100644
--- a/r/sedonadb/man/sd_configure_proj.Rd
+++ b/r/sedonadb/man/sd_configure_proj.Rd
@@ -14,6 +14,7 @@ sd_configure_proj(
 \arguments{
 \item{preset}{One of:
 \itemize{
+\item \code{"conda"}: Look for PROJ in \code{CONDA_PREFIX}.
 \item \code{"homebrew"}: Look for PROJ installed by Homebrew. This is the easiest
 option on MacOS.
 \item \code{"system"}: Look for PROJ in the platform library load path (e.g.,
diff --git a/r/sedonadb/src/Makevars.win.in b/r/sedonadb/src/Makevars.win.in
index 20c6cbdf1..cf093b27d 100644
--- a/r/sedonadb/src/Makevars.win.in
+++ b/r/sedonadb/src/Makevars.win.in
@@ -27,6 +27,8 @@ VCPKG_MANIFEST_MODE = OFF
 CMAKE_TOOLCHAIN_FILE = @CMAKE_TOOLCHAIN_FILE@
 CMAKE_PREFIX_PATH = @CMAKE_PREFIX_PATH@
 OPENSSL_ROOT_DIR = @OPENSSL_ROOT_DIR@
+SPATIAL_VCPKG_PREFIX = @SPATIAL_VCPKG_PREFIX@
+GDAL_VERSION = @GDAL_VERSION@
 SEDONADB_R_CC = $(strip $(CC))
 SEDONADB_R_CXX = $(strip $(CXX17))
 SEDONADB_R_CFLAGS = $(CPPFLAGS) $(CFLAGS)
@@ -81,7 +83,7 @@ PKG_CONFIG_SYSROOT_DIR = /x86_64-w64-mingw32.static.posix/
 # also incompatible with aws-lc-sys.
 CFLAGS_TWEAKED = $(filter-out -O%,$(CFLAGS)) -Wno-pedantic
 
-all: $(SHLIB) clean_intermediate
+all: $(SHLIB) copy_native_data clean_intermediate
 
 $(SHLIB): $(STATLIB)
 
@@ -94,6 +96,16 @@ $(STATLIB):
 	export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \
 	  export LIBRARY_PATH="$${LIBRARY_PATH};$(LIBDIR)/libgcc_mock" && \
 	  export PKG_CONFIG_SYSROOT_DIR="$(PKG_CONFIG_SYSROOT_DIR)" && \
+	  if [ -n "$(SPATIAL_VCPKG_PREFIX)" ]; then \
+	    export PKG_CONFIG_ALLOW_CROSS=1; \
+	    export PKG_CONFIG_ALL_STATIC=1; \
+	    export PKG_CONFIG_SYSROOT_DIR=""; \
+	    export PKG_CONFIG_PATH="$(SPATIAL_VCPKG_PREFIX)/lib/pkgconfig:$(SPATIAL_VCPKG_PREFIX)/share/pkgconfig$${PKG_CONFIG_PATH:+:$${PKG_CONFIG_PATH}}"; \
+	    export GDAL_STATIC=1; \
+	    export GDAL_INCLUDE_DIR="$(SPATIAL_VCPKG_PREFIX)/include"; \
+	    export GDAL_LIB_DIR="$(SPATIAL_VCPKG_PREFIX)/lib"; \
+	    export GDAL_VERSION="$(GDAL_VERSION)"; \
+	  fi && \
 	  if [ -n "$(VCPKG_ROOT)" ]; then \
 	    export VCPKG_ROOT="$(VCPKG_ROOT)"; \
 	    export VCPKG_DEFAULT_TRIPLET="$(VCPKG_TARGET_TRIPLET)"; \
@@ -119,7 +131,18 @@ $(STATLIB):
 clean_intermediate: $(SHLIB)
 	rm -f $(STATLIB)
 
+# GDAL and PROJ runtime data must match the vcpkg libraries bundled above.
+# Copying these trees follows the Windows packaging pattern used by sf/vapour.
+copy_native_data:
+	if [ -n "$(SPATIAL_VCPKG_PREFIX)" ]; then \
+	  test -d "$(SPATIAL_VCPKG_PREFIX)/share/gdal"; \
+	  test -d "$(SPATIAL_VCPKG_PREFIX)/share/proj"; \
+	  rm -rf ../inst/gdal ../inst/proj; \
+	  cp -R "$(SPATIAL_VCPKG_PREFIX)/share/gdal" ../inst/gdal; \
+	  cp -R "$(SPATIAL_VCPKG_PREFIX)/share/proj" ../inst/proj; \
+	fi
+
 clean:
 	rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target
 
-.PHONY: all clean_intermediate clean
+.PHONY: all copy_native_data clean_intermediate clean
diff --git a/r/sedonadb/src/init.c b/r/sedonadb/src/init.c
index 5a295dc57..c12dedb3d 100644
--- a/r/sedonadb/src/init.c
+++ b/r/sedonadb/src/init.c
@@ -73,6 +73,27 @@ SEXP savvy_configure_proj_shared__impl(SEXP c_arg__shared_library_path,
   return handle_result(res);
 }
 
+SEXP savvy_configure_gdal_prefix_native__impl(SEXP c_arg__prefix) {
+  return handle_result(savvy_configure_gdal_prefix_native__ffi(c_arg__prefix));
+}
+
+SEXP savvy_configure_gdal_shared__impl(SEXP c_arg__shared_library_path) {
+  return handle_result(
+      savvy_configure_gdal_shared__ffi(c_arg__shared_library_path));
+}
+
+SEXP savvy_configure_gdal_system_native__impl(void) {
+  return handle_result(savvy_configure_gdal_system_native__ffi());
+}
+
+SEXP savvy_configure_proj_prefix_native__impl(SEXP c_arg__prefix) {
+  return handle_result(savvy_configure_proj_prefix_native__ffi(c_arg__prefix));
+}
+
+SEXP savvy_configure_proj_system_native__impl(void) {
+  return handle_result(savvy_configure_proj_system_native__ffi());
+}
+
 SEXP savvy_init_r_runtime__impl(DllInfo *c_arg___dll_info) {
   SEXP res = savvy_init_r_runtime__ffi(c_arg___dll_info);
   return handle_result(res);
@@ -404,6 +425,16 @@ static const R_CallMethodDef CallEntries[] = {
      (DL_FUNC)&savvy_apply_crses_to_sf_stream__impl, 4},
     {"savvy_configure_proj_shared__impl",
      (DL_FUNC)&savvy_configure_proj_shared__impl, 3},
+    {"savvy_configure_gdal_prefix_native__impl",
+     (DL_FUNC)&savvy_configure_gdal_prefix_native__impl, 1},
+    {"savvy_configure_gdal_shared__impl",
+     (DL_FUNC)&savvy_configure_gdal_shared__impl, 1},
+    {"savvy_configure_gdal_system_native__impl",
+     (DL_FUNC)&savvy_configure_gdal_system_native__impl, 0},
+    {"savvy_configure_proj_prefix_native__impl",
+     (DL_FUNC)&savvy_configure_proj_prefix_native__impl, 1},
+    {"savvy_configure_proj_system_native__impl",
+     (DL_FUNC)&savvy_configure_proj_system_native__impl, 0},
     {"savvy_init_r_runtime_interrupts__impl",
      (DL_FUNC)&savvy_init_r_runtime_interrupts__impl, 2},
     {"savvy_sedonadb_adbc_init_func__impl",
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/src/install.libs.R
old mode 100755
new mode 100644
similarity index 69%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/src/install.libs.R
index e81e481cb..63784e17e
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/src/install.libs.R
@@ -15,4 +15,14 @@
 # specific language governing permissions and limitations
 # under the License.
 
-rm -f src/Makevars.win
+destination <- file.path(R_PACKAGE_DIR, paste0("libs", R_ARCH))
+dir.create(destination, recursive = TRUE, showWarnings = FALSE)
+
+package_dll <- Sys.glob(paste0("*", SHLIB_EXT))
+
+if (
+  length(package_dll) == 0L ||
+    !all(file.copy(package_dll, destination, overwrite = TRUE))
+) {
+  stop("Failed to install the sedonadb native library")
+}
diff --git a/r/sedonadb/src/rust/Cargo.toml b/r/sedonadb/src/rust/Cargo.toml
index 8301bf93a..490f28f04 100644
--- a/r/sedonadb/src/rust/Cargo.toml
+++ b/r/sedonadb/src/rust/Cargo.toml
@@ -26,6 +26,9 @@ crate-type = ["staticlib", "lib"]
 
 [features]
 s2geography = ["sedona/s2geography"]
+gdal = ["sedona/gdal"]
+bindgen = ["sedona/bindgen"]
+proj = ["sedona/proj"]
 
 [dependencies]
 arrow-array = { workspace = true }
@@ -43,6 +46,7 @@ sedona-expr = { workspace = true }
 sedona-extension = { workspace = true }
 sedona-geometry = { workspace = true }
 sedona-geoparquet = { workspace = true }
+sedona-gdal = { workspace = true }
 sedona-proj = { workspace = true }
 sedona-schema = { workspace = true }
 thiserror = { workspace = true }
diff --git a/r/sedonadb/src/rust/api.h b/r/sedonadb/src/rust/api.h
index 602bf959f..7eec679c7 100644
--- a/r/sedonadb/src/rust/api.h
+++ b/r/sedonadb/src/rust/api.h
@@ -22,6 +22,11 @@ SEXP savvy_apply_crses_to_sf_stream__ffi(SEXP c_arg__stream_in_xptr,
 SEXP savvy_configure_proj_shared__ffi(SEXP c_arg__shared_library_path,
                                       SEXP c_arg__database_path,
                                       SEXP c_arg__search_path);
+SEXP savvy_configure_gdal_prefix_native__ffi(SEXP c_arg__prefix);
+SEXP savvy_configure_gdal_shared__ffi(SEXP c_arg__shared_library_path);
+SEXP savvy_configure_gdal_system_native__ffi(void);
+SEXP savvy_configure_proj_prefix_native__ffi(SEXP c_arg__prefix);
+SEXP savvy_configure_proj_system_native__ffi(void);
 SEXP savvy_init_r_runtime__ffi(DllInfo *c_arg___dll_info);
 SEXP savvy_init_r_runtime_interrupts__ffi(SEXP c_arg__interrupts_call,
                                           SEXP c_arg__pkg_env);
diff --git a/r/sedonadb/src/rust/src/lib.rs b/r/sedonadb/src/rust/src/lib.rs
index 0db3f2b75..3ce19bb12 100644
--- a/r/sedonadb/src/rust/src/lib.rs
+++ b/r/sedonadb/src/rust/src/lib.rs
@@ -20,7 +20,9 @@ use std::ffi::c_void;
 use savvy::savvy;
 
 use savvy_ffi::R_NilValue;
+use sedona::native_library::NativeLibrary;
 use sedona_adbc::AdbcSedonadbDriverInit;
+use sedona_gdal::global::{configure_global_gdal_api, GdalApiBuilder};
 use sedona_proj::register::{configure_global_proj_engine, ProjCrsEngineBuilder};
 
 mod context;
@@ -64,6 +66,68 @@ fn configure_proj_shared(
         builder = builder.with_search_paths(vec![search_path.into()]);
     }
 
+    builder.build()?;
     configure_global_proj_engine(builder)?;
     Ok(())
 }
+
+#[savvy]
+fn configure_proj_prefix_native(prefix: &str) -> savvy::Result<()> {
+    let installation = NativeLibrary::Proj
+        .from_prefix(prefix.as_ref())
+        .map_err(savvy::Error::GeneralError)?;
+    configure_proj_shared(
+        Some(installation.shared_library.to_string_lossy().as_ref()),
+        Some(
+            installation
+                .data_dir
+                .join("proj.db")
+                .to_string_lossy()
+                .as_ref(),
+        ),
+        Some(installation.data_dir.to_string_lossy().as_ref()),
+    )
+}
+
+#[savvy]
+fn configure_proj_system_native() -> savvy::Result<()> {
+    let mut errors = Vec::new();
+    for candidate in NativeLibrary::Proj.system_candidates() {
+        match configure_proj_shared(Some(candidate.to_string_lossy().as_ref()), None, None) {
+            Ok(()) => return Ok(()),
+            Err(e) => errors.push(e.to_string()),
+        }
+    }
+    Err(savvy::Error::GeneralError(errors.join("; ")))
+}
+
+#[savvy]
+fn configure_gdal_shared(shared_library_path: Option<&str>) -> savvy::Result<()> {
+    let mut builder = GdalApiBuilder::default();
+    if let Some(shared_library_path) = shared_library_path {
+        builder = builder.with_shared_library(shared_library_path.into());
+    }
+    builder.build()?;
+    configure_global_gdal_api(builder)?;
+    Ok(())
+}
+
+#[savvy]
+fn configure_gdal_prefix_native(prefix: &str) -> savvy::Result<()> {
+    let installation = NativeLibrary::Gdal
+        .from_prefix(prefix.as_ref())
+        .map_err(savvy::Error::GeneralError)?;
+    configure_gdal_shared(Some(installation.shared_library.to_string_lossy().as_ref()))
+}
+
+#[savvy]
+fn configure_gdal_system_native() -> savvy::Result<()> {
+    let mut errors = Vec::new();
+    for candidate in NativeLibrary::Gdal.system_candidates() {
+        match configure_gdal_shared(Some(candidate.to_string_lossy().as_ref())) {
+            Ok(()) => return Ok(()),
+            Err(e) => errors.push(e.to_string()),
+        }
+    }
+    Err(savvy::Error::GeneralError(errors.join("; ")))
+}
diff --git a/r/sedonadb/tests/testthat/test-context.R b/r/sedonadb/tests/testthat/test-context.R
index 6eb692e8b..7fa327855 100644
--- a/r/sedonadb/tests/testthat/test-context.R
+++ b/r/sedonadb/tests/testthat/test-context.R
@@ -201,6 +201,18 @@ test_that("configure_proj() errors for invalid inputs", {
   )
 })
 
+test_that("configure_gdal() errors for invalid inputs", {
+  expect_error(
+    sd_configure_gdal("not a preset"),
+    "Unknown preset"
+  )
+
+  expect_error(
+    sd_configure_gdal(shared_library = "file that does not exist"),
+    "Failed to load GDAL"
+  )
+})
+
 test_that(".fns can have its contents listed", {
   expect_contains(names(.fns), "st_intersects")
   expect_contains(.DollarNames(.fns, "st_int"), "st_intersects")
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
old mode 100755
new mode 100644
similarity index 84%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
index e81e481cb..ec1099fe7
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-arm64-mingw-spatial-release.cmake
@@ -15,4 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-rm -f src/Makevars.win
+set(VCPKG_TARGET_ARCHITECTURE arm64)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+include("${CMAKE_CURRENT_LIST_DIR}/sedonadb-mingw-static-release.cmake")
diff --git a/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake b/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
index 7dd44e70c..0db28e1cc 100644
--- a/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-mingw-static-release.cmake
@@ -16,7 +16,9 @@
 # under the License.
 
 set(VCPKG_CRT_LINKAGE dynamic)
-set(VCPKG_LIBRARY_LINKAGE static)
+if(NOT DEFINED VCPKG_LIBRARY_LINKAGE)
+  set(VCPKG_LIBRARY_LINKAGE static)
+endif()
 set(VCPKG_BUILD_TYPE release)
 set(VCPKG_CMAKE_SYSTEM_NAME MinGW)
 
diff --git a/r/sedonadb/cleanup.win b/r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
old mode 100755
new mode 100644
similarity index 84%
copy from r/sedonadb/cleanup.win
copy to r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
index e81e481cb..d0fdcb793
--- a/r/sedonadb/cleanup.win
+++ b/r/sedonadb/tools/vcpkg-triplets/sedonadb-x64-mingw-spatial-release.cmake
@@ -15,4 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-rm -f src/Makevars.win
+set(VCPKG_TARGET_ARCHITECTURE x64)
+set(VCPKG_LIBRARY_LINKAGE static)
+
+include("${CMAKE_CURRENT_LIST_DIR}/sedonadb-mingw-static-release.cmake")
diff --git a/rust/sedona/src/lib.rs b/rust/sedona/src/lib.rs
index 907848843..6e15c3f8f 100644
--- a/rust/sedona/src/lib.rs
+++ b/rust/sedona/src/lib.rs
@@ -20,6 +20,7 @@ pub mod context;
 pub mod context_builder;
 mod exec;
 pub mod memory_pool;
+pub mod native_library;
 mod object_storage;
 pub use object_storage::is_object_store_option;
 pub mod pool_type;
diff --git a/rust/sedona/src/native_library.rs b/rust/sedona/src/native_library.rs
new file mode 100644
index 000000000..520605bac
--- /dev/null
+++ b/rust/sedona/src/native_library.rs
@@ -0,0 +1,210 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Shared runtime discovery for native GDAL and PROJ installations.
+//!
+//! Language bindings may add package-specific discovery (for example, Python
+//! wheels bundled by rasterio or pyproj), but prefix-based installations such
+//! as Conda and Homebrew should resolve identically in every binding.
+
+use std::fmt::{Display, Formatter};
+use std::path::{Path, PathBuf};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum NativeLibrary {
+    Gdal,
+    Proj,
+}
+
+impl Display for NativeLibrary {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Gdal => f.write_str("GDAL"),
+            Self::Proj => f.write_str("PROJ"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct NativeLibraryInstallation {
+    pub shared_library: PathBuf,
+    pub data_dir: PathBuf,
+}
+
+impl NativeLibrary {
+    pub fn system_library_name(self) -> &'static str {
+        self.system_library_names()[0]
+    }
+
+    pub fn system_library_names(self) -> &'static [&'static str] {
+        match (self, std::env::consts::OS) {
+            (Self::Gdal, "windows") => &["gdal.dll"],
+            (Self::Gdal, "macos") => &["libgdal.dylib"],
+            (Self::Gdal, _) => &["libgdal.so"],
+            (Self::Proj, "windows") => &["proj.dll", "proj_9.dll"],
+            (Self::Proj, "macos") => &["libproj.dylib"],
+            (Self::Proj, _) => &["libproj.so"],
+        }
+    }
+
+    /// Return loadable names and discovered files in platform library paths.
+    ///
+    /// The directory scan matters on Linux distributions that install only a
+    /// versioned runtime library (for example `libgdal.so.34`) unless the
+    /// separate development package is installed.
+    pub fn system_candidates(self) -> Vec<PathBuf> {
+        let mut candidates = self
+            .system_library_names()
+            .iter()
+            .map(PathBuf::from)
+            .collect::<Vec<_>>();
+
+        let search_var = match std::env::consts::OS {
+            "windows" => "PATH",
+            "macos" => "DYLD_LIBRARY_PATH",
+            _ => "LD_LIBRARY_PATH",
+        };
+        let mut search_dirs = std::env::var_os(search_var)
+            .map(|paths| std::env::split_paths(&paths).collect::<Vec<_>>())
+            .unwrap_or_default();
+
+        if cfg!(target_os = "linux") {
+            search_dirs.extend([
+                PathBuf::from("/usr/local/lib"),
+                PathBuf::from("/usr/lib"),
+                PathBuf::from("/lib"),
+                PathBuf::from(format!("/usr/lib/{}-linux-gnu", std::env::consts::ARCH)),
+                PathBuf::from(format!("/lib/{}-linux-gnu", std::env::consts::ARCH)),
+            ]);
+        } else if cfg!(target_os = "macos") {
+            search_dirs.extend([
+                PathBuf::from("/opt/homebrew/lib"),
+                PathBuf::from("/usr/local/lib"),
+            ]);
+        }
+
+        for dir in search_dirs {
+            if let Ok(path) = self.find_shared_library(&dir) {
+                if !candidates.contains(&path) {
+                    candidates.push(path);
+                }
+            }
+        }
+
+        candidates
+    }
+
+    fn glob_parts(self) -> (&'static str, &'static str) {
+        match (self, std::env::consts::OS) {
+            (Self::Gdal, "windows") => ("gdal", ".dll"),
+            (Self::Gdal, "macos") => ("libgdal", ".dylib"),
+            (Self::Gdal, _) => ("libgdal.so", ""),
+            (Self::Proj, "windows") => ("proj", ".dll"),
+            (Self::Proj, "macos") => ("libproj", ".dylib"),
+            (Self::Proj, _) => ("libproj.so", ""),
+        }
+    }
+
+    pub fn from_prefix(self, prefix: &Path) -> Result<NativeLibraryInstallation, String> {
+        if prefix.as_os_str().is_empty() || !prefix.is_dir() {
+            return Err(format!(
+                "Can't configure {self} from prefix '{}': does not exist",
+                prefix.display()
+            ));
+        }
+
+        let (lib_dir, share_dir) = if cfg!(windows) {
+            (prefix.join("Library/bin"), prefix.join("Library/share"))
+        } else {
+            (prefix.join("lib"), prefix.join("share"))
+        };
+
+        let data_dir = share_dir.join(match self {
+            Self::Gdal => "gdal",
+            Self::Proj => "proj",
+        });
+        if !data_dir.is_dir() {
+            return Err(format!(
+                "Can't find {self} data directory '{}'",
+                data_dir.display()
+            ));
+        }
+
+        Ok(NativeLibraryInstallation {
+            shared_library: self.find_shared_library(&lib_dir)?,
+            data_dir,
+        })
+    }
+
+    pub fn find_shared_library(self, lib_dir: &Path) -> Result<PathBuf, String> {
+        let (prefix, suffix) = self.glob_parts();
+        let entries = std::fs::read_dir(lib_dir).map_err(|e| {
+            format!(
+                "Can't read {self} library directory '{}': {e}",
+                lib_dir.display()
+            )
+        })?;
+
+        let mut matches = entries
+            .filter_map(Result::ok)
+            .map(|entry| entry.path())
+            .filter(|path| {
+                let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
+                    return false;
+                };
+                name.starts_with(prefix)
+                    && (suffix.is_empty() || name.ends_with(suffix))
+                    && !name.contains("_d.")
+            })
+            .collect::<Vec<_>>();
+
+        matches.sort_by_key(|path| {
+            path.file_name()
+                .map(|name| name.len())
+                .unwrap_or(usize::MAX)
+        });
+        matches.into_iter().next().ok_or_else(|| {
+            format!(
+                "Can't find {self} shared library in '{}'",
+                lib_dir.display()
+            )
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn system_names_match_platform() {
+        let gdal = NativeLibrary::Gdal.system_library_name();
+        let proj = NativeLibrary::Proj.system_library_name();
+        assert!(gdal.to_ascii_lowercase().contains("gdal"));
+        assert!(proj.to_ascii_lowercase().contains("proj"));
+        assert!(!NativeLibrary::Gdal.system_candidates().is_empty());
+    }
+
+    #[test]
+    fn missing_prefix_has_useful_error() {
+        let err = NativeLibrary::Proj
+            .from_prefix(Path::new("/path/that/does/not/exist"))
+            .unwrap_err();
+        assert!(err.contains("PROJ"));
+        assert!(err.contains("does not exist"));
+    }
+}