Repository: nmandery/h3ronpy (language: python)

Issue:
Move to official arrow bindings
... once https://github.com/nmandery/h3arrow/pull/3 has been merged.


Discussion / hints:

Fix patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3064853..14b46ca 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -12,6 +12,11 @@ Versioning <https://semver.org/spec/v2.0.0.html>`__.
 Unreleased
 ----------
 
+- Migrate from the unmaintained arrow2 arrow implementation to the official arrow-rs from apache. This comes along
+  with a few changes:
+  - `directededges_to_wkb_lines` and `directededges_to_lines` have been removed. Use the linestring-versions instead.
+  - Geometry collections are currently unsupported when working with WKB. This is still work-in-progress within geoarrow-rs.
+
 0.19.2 - 2023-11-16
 -------------------
 
diff --git a/Cargo.toml b/Cargo.toml
index 3011ed6..cccbb0c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,11 +14,12 @@ name = "h3ronpy"
 crate-type = ["cdylib"]
 
 [dependencies]
-env_logger = "^0.10"
+arrow = { version = "50.0.0", features = ["pyarrow"]}
+env_logger = "^0.11"
 geo-types = "^0.7"
-rasterh3 = "^0.5"
+rasterh3 = { version = "^0.6", features = ["rayon"] }
 #h3arrow = { version = "0.2.0", features = ["geoarrow", "rayon"] }
-h3arrow = { git = "https://github.com/nmandery/h3arrow.git", rev = "ec8cf2fc4028016dc784a4e9d660af7222aff126", features = ["geoarrow", "rayon"] }
+h3arrow = { git = "https://github.com/nmandery/h3arrow.git", rev = "1491610246cf93f0c9e40974de857563451a2e03", features = ["geoarrow", "rayon"] }
 #h3arrow = { path = "/home/nicodev/CLionProjects/h3arrow", features = ["geoarrow", "rayon"] }
 ndarray = { version = "0.15", features = ["rayon"] }
 numpy = "0.20"
@@ -27,7 +28,7 @@ py_geo_interface = { version = "0.7", features = ["f64", "wkb"] }
 pyo3 = { version = "0.20", features = ["extension-module", "abi3", "abi3-py38"] }
 rayon = "^1.5"
 hashbrown = "0.14"
-geo = "0.26"
+geo = "0.27"
 itertools = "0.12"
 
 
diff --git a/justfile b/justfile
index 1edafb7..cd17c1a 100644
--- a/justfile
+++ b/justfile
@@ -10,4 +10,4 @@ test:
     rm -f dist/*.whl
     maturin build --out dist
     pip install --force-reinstall dist/*.whl
-    pytest -s
\ No newline at end of file
+    RUST_BACKTRACE=1 pytest -s
\ No newline at end of file
diff --git a/python/h3ronpy/arrow/vector.py b/python/h3ronpy/arrow/vector.py
index bb09b10..a2d5cf0 100644
--- a/python/h3ronpy/arrow/vector.py
+++ b/python/h3ronpy/arrow/vector.py
@@ -84,18 +84,6 @@ def vertexes_to_wkb_points(arr, radians: bool = False) -> pa.Array:
     return vector.vertexes_to_wkb_points(_to_uint64_array(arr), radians=radians)
 
 
-def directededges_to_wkb_lines(arr, radians: bool = False) -> pa.Array:
-    """
-    Convert directed edges to lines.
-
-    The returned geometries in the output array will match the order of the input array.
-
-    :param: arr: The directed edge array
-    :param radians: Generate geometries using radians instead of degrees
-    """
-    return vector.directededges_to_wkb_lines(_to_uint64_array(arr), radians=radians)
-
-
 def directededges_to_wkb_linestrings(arr, radians: bool = False) -> pa.Array:
     """
     Convert directed edges to linestrings.
@@ -186,7 +174,6 @@ def geometry_to_cells(
     cells_to_wkb_points.__name__,
     vertexes_to_wkb_points.__name__,
     directededges_to_wkb_linestrings.__name__,
-    directededges_to_wkb_lines.__name__,
     wkb_to_cells.__name__,
     geometry_to_cells.__name__,
 ]
diff --git a/python/h3ronpy/pandas/vector.py b/python/h3ronpy/pandas/vector.py
index fc5439d..86d50b3 100644
--- a/python/h3ronpy/pandas/vector.py
+++ b/python/h3ronpy/pandas/vector.py
@@ -48,12 +48,6 @@ def wrapper(*args, **kw):
     doc="Create a geoseries containing the point geometries of a vertex array",
     name="vertexes_to_points",
 )
-directededges_to_wkb_lines = _wrap(_av.directededges_to_wkb_lines, ret_type=pd.Series)
-directededges_to_lines = _geoseries_from_wkb(
-    directededges_to_wkb_lines,
-    doc="Create a geoseries containing the line geometries of a directededge array",
-    name="directededges_to_lines",
-)
 directededges_to_wkb_linestrings = _wrap(_av.directededges_to_wkb_linestrings, ret_type=pd.Series)
 directededges_to_linestrings = _geoseries_from_wkb(
     directededges_to_wkb_linestrings,
@@ -140,8 +134,6 @@ def geodataframe_to_cells(
     vertexes_to_points.__name__,
     directededges_to_wkb_linestrings.__name__,
     directededges_to_linestrings.__name__,
-    directededges_to_wkb_lines.__name__,
-    directededges_to_lines.__name__,
     cells_dataframe_to_geodataframe.__name__,
     wkb_to_cells.__name__,
     geometry_to_cells.__name__,
diff --git a/python/h3ronpy/polars/vector.py b/python/h3ronpy/polars/vector.py
index 8526ec0..9c734ee 100644
--- a/python/h3ronpy/polars/vector.py
+++ b/python/h3ronpy/polars/vector.py
@@ -9,8 +9,6 @@
 cells_to_wkb_polygons = _wrap(_av.cells_to_wkb_polygons, ret_type=pl.Series)
 cells_to_wkb_points = _wrap(_av.cells_to_wkb_points, ret_type=pl.Series)
 vertexes_to_wkb_points = _wrap(_av.vertexes_to_wkb_points, ret_type=pl.Series)
-directededges_to_wkb_lines = _wrap(_av.directededges_to_wkb_lines, ret_type=pl.Series)
-directededges_to_wkb_linestrings = _wrap(_av.directededges_to_wkb_linestrings, ret_type=pl.Series)
 wkb_to_cells = _wrap(_av.wkb_to_cells, ret_type=pl.Series)
 geometry_to_cells = _wrap(_av.geometry_to_cells, ret_type=pl.Series)
 
@@ -22,8 +20,6 @@
     cells_to_wkb_polygons.__name__,
     cells_to_wkb_points.__name__,
     vertexes_to_wkb_points.__name__,
-    directededges_to_wkb_lines.__name__,
-    directededges_to_wkb_linestrings.__name__,
     wkb_to_cells.__name__,
     geometry_to_cells.__name__,
 ]
diff --git a/src/arrow_interop.rs b/src/arrow_interop.rs
index e60052c..98d70e3 100644
--- a/src/arrow_interop.rs
+++ b/src/arrow_interop.rs
@@ -1,13 +1,11 @@
+use arrow::array::{make_array, Array, ArrayData, UInt64Array};
+use arrow::pyarrow::{FromPyArrow, IntoPyArrow};
 use std::any::{type_name, Any};
 
 use h3arrow::array::{
     CellIndexArray, DirectedEdgeIndexArray, H3Array, H3IndexArrayValue, VertexIndexArray,
 };
-use h3arrow::export::arrow2::array::{Array, PrimitiveArray, UInt64Array};
-use h3arrow::export::arrow2::datatypes::Field;
-use h3arrow::export::arrow2::ffi;
 use pyo3::exceptions::PyValueError;
-use pyo3::ffi::Py_uintptr_t;
 use pyo3::prelude::*;
 use pyo3::Python;
 
@@ -23,70 +21,24 @@ where
     })
 }
 
-/// Arrow array to Python.
-/// from https://github.com/pola-rs/polars/blob/d1e5b1062c6872cd030b04b96505d2fac36b5376/py-polars/src/arrow_interop/to_py.rs
-pub(crate) fn native_to_pyarray(
-    array: Box<dyn Array>,
-    py: Python,
-    pyarrow: &PyModule,
-) -> PyResult<PyObject> {
-    let schema = Box::new(ffi::export_field_to_c(&Field::new(
-        "",
-        array.data_type().clone(),
-        true,
-    )));
-    let array = Box::new(ffi::export_array_to_c(array));
-
-    let schema_ptr: *const ffi::ArrowSchema = &*schema;
-    let array_ptr: *const ffi::ArrowArray = &*array;
-
-    let array = pyarrow.getattr("Array")?.call_method1(
-        "_import_from_c",
-        (array_ptr as Py_uintptr_t, schema_ptr as Py_uintptr_t),
-    )?;
-
-    Ok(array.to_object(py))
-}
-
 #[inline]
-pub fn h3array_to_pyarray<IX>(
-    h3array: H3Array<IX>,
-    py: Python,
-    pyarrow: &PyModule,
-) -> PyResult<PyObject>
+pub fn h3array_to_pyarray<IX>(h3array: H3Array<IX>, py: Python) -> PyResult<PyObject>
 where
     IX: H3IndexArrayValue,
 {
-    native_to_pyarray(PrimitiveArray::from(h3array).boxed(), py, pyarrow)
+    let pa: UInt64Array = h3array.into();
+    pa.into_data().into_pyarrow(py)
 }
 
-pub(crate) fn pyarray_to_boxed(obj: &PyAny) -> PyResult<Box<dyn Array>> {
-    // prepare a pointer to receive the Array struct
-    let array = Box::new(ffi::ArrowArray::empty());
-    let schema = Box::new(ffi::ArrowSchema::empty());
-
-    let array_ptr = &*array as *const ffi::ArrowArray;
-    let schema_ptr = &*schema as *const ffi::ArrowSchema;
-
-    // make the conversion through PyArrow's private API
-    // this changes the pointer's memory and is thus unsafe. In particular, `_export_to_c` can go out of bounds
-    obj.call_method1(
-        "_export_to_c",
-        (array_ptr as Py_uintptr_t, schema_ptr as Py_uintptr_t),
-    )?;
-
-    let array = unsafe {
-        let field = ffi::import_field_from_c(schema.as_ref()).into_pyresult()?;
-        ffi::import_array_from_c(*array, field.data_type).into_pyresult()?
-    };
-    Ok(array)
+#[inline]
+pub(crate) fn pyarray_to_boxed(obj: &PyAny) -> PyResult<ArrayData> {
+    ArrayData::from_pyarrow(obj)
 }
 
 pub(crate) fn pyarray_to_native<T: Any + Array + Clone>(obj: &PyAny) -> PyResult<T> {
-    let array = pyarray_to_boxed(obj)?;
+    let array = make_array(pyarray_to_boxed(obj)?);
 
-    // downcast to the concrete type
-    Ok(array
+    let array = array
         .as_any()
         .downcast_ref::<T>()
         .ok_or_else(|| {
@@ -96,7 +48,10 @@ pub(crate) fn pyarray_to_native<T: Any + Array + Clone>(obj: &PyAny) -> PyResult
                 array.data_type(),
             ))
         })?
-        .clone())
+        .clone();
+
+    // downcast to the concrete type
+    Ok(array)
 }
 
 pub(crate) fn pyarray_to_cellindexarray(obj: &PyAny) -> PyResult<CellIndexArray> {
diff --git a/src/error.rs b/src/error.rs
index 70e0b6f..23ab29e 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -11,7 +11,7 @@ pub trait IntoPyErr {
     fn into_pyerr(self) -> PyErr;
 }
 
-impl IntoPyErr for h3arrow::export::arrow2::error::Error {
+impl IntoPyErr for h3arrow::export::arrow::error::ArrowError {
     fn into_pyerr(self) -> PyErr {
         PyRuntimeError::new_err(self.to_string())
     }
@@ -29,7 +29,7 @@ impl IntoPyErr for A3Error {
             A3Error::CompactionError(e) => e.into_pyerr(),
             A3Error::OutlinerError(e) => e.into_pyerr(),
             A3Error::Arrow2(e) => e.into_pyerr(),
-            A3Error::NotAPrimitiveArrayU64
+            A3Error::NotAUint64Array
             | A3Error::NonParsableCellIndex
             | A3Error::NonParsableDirectedEdgeIndex
             | A3Error::NonParsableVertexIndex
diff --git a/src/op/compact.rs b/src/op/compact.rs
index 25ec3cb..d0a92f9 100644
--- a/src/op/compact.rs
+++ b/src/op/compact.rs
@@ -16,7 +16,7 @@ pub(crate) fn compact(cellarray: &PyAny, mixed_resolutions: bool) -> PyResult<Py
     }
     .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(compacted, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(compacted, py))
 }
 
 #[pyfunction]
@@ -24,5 +24,5 @@ pub(crate) fn compact(cellarray: &PyAny, mixed_resolutions: bool) -> PyResult<Py
 pub(crate) fn uncompact(cellarray: &PyAny, target_resolution: u8) -> PyResult<PyObject> {
     let target_resolution = Resolution::try_from(target_resolution).into_pyresult()?;
     let out = pyarray_to_cellindexarray(cellarray)?.uncompact(target_resolution);
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(out, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(out, py))
 }
diff --git a/src/op/measure.rs b/src/op/measure.rs
index f6037a7..b079d47 100644
--- a/src/op/measure.rs
+++ b/src/op/measure.rs
@@ -1,23 +1,25 @@
-use crate::arrow_interop::{native_to_pyarray, pyarray_to_cellindexarray, with_pyarrow};
+use crate::arrow_interop::pyarray_to_cellindexarray;
+use arrow::array::Array;
+use arrow::pyarrow::IntoPyArrow;
 use pyo3::prelude::*;
 
 #[pyfunction]
 #[pyo3(signature = (cellarray))]
 pub(crate) fn cells_area_m2(cellarray: &PyAny) -> PyResult<PyObject> {
     let out = pyarray_to_cellindexarray(cellarray)?.area_m2();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out.boxed(), py, pyarrow))
+    Python::with_gil(|py| out.into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (cellarray))]
 pub(crate) fn cells_area_km2(cellarray: &PyAny) -> PyResult<PyObject> {
     let out = pyarray_to_cellindexarray(cellarray)?.area_km2();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out.boxed(), py, pyarrow))
+    Python::with_gil(|py| out.into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (cellarray))]
 pub(crate) fn cells_area_rads2(cellarray: &PyAny) -> PyResult<PyObject> {
     let out = pyarray_to_cellindexarray(cellarray)?.area_rads2();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out.boxed(), py, pyarrow))
+    Python::with_gil(|py| out.into_data().into_pyarrow(py))
 }
diff --git a/src/op/neighbor.rs b/src/op/neighbor.rs
index 3803587..a2b0e09 100644
--- a/src/op/neighbor.rs
+++ b/src/op/neighbor.rs
@@ -1,5 +1,6 @@
+use arrow::array::{Array, GenericListArray, LargeListArray, PrimitiveArray, UInt32Array};
+use arrow::pyarrow::{IntoPyArrow, ToPyArrow};
 use h3arrow::algorithm::{GridDiskDistances, GridOp, KAggregationMethod};
-use h3arrow::export::arrow2::array::{Array, ListArray, PrimitiveArray};
 use pyo3::exceptions::{PyRuntimeError, PyValueError};
 use pyo3::{PyObject, PyResult};
 use std::str::FromStr;
@@ -16,11 +17,9 @@ pub(crate) fn grid_disk(cellarray: &PyAny, k: u32, flatten: bool) -> PyResult<Py
     let listarray = cellindexarray.grid_disk(k).into_pyresult()?;
     if flatten {
         let cellindexarray = listarray.into_flattened().into_pyresult()?;
-        with_pyarrow(|py, pyarrow| h3array_to_pyarray(cellindexarray, py, pyarrow))
+        Python::with_gil(|py| h3array_to_pyarray(cellindexarray, py))
     } else {
-        with_pyarrow(|py, pyarrow| {
-            native_to_pyarray(ListArray::from(listarray).boxed(), py, pyarrow)
-        })
+        Python::with_gil(|py| LargeListArray::from(listarray).into_data().to_pyarrow(py))
     }
 }
 
@@ -53,32 +52,30 @@ pub(crate) fn grid_ring_distances(
 }
 
 fn return_griddiskdistances_table(
-    griddiskdistances: GridDiskDistances,
+    griddiskdistances: GridDiskDistances<i64>,
     flatten: bool,
 ) -> PyResult<PyObject> {
     let (cells, distances) = if flatten {
         (
-            PrimitiveArray::from(griddiskdistances.cells.into_flattened().into_pyresult()?).boxed(),
+            PrimitiveArray::from(griddiskdistances.cells.into_flattened().into_pyresult()?)
+                .into_data(),
             griddiskdistances
                 .distances
                 .values()
                 .as_any()
-                .downcast_ref::<PrimitiveArray<u32>>()
+                .downcast_ref::<UInt32Array>()
                 .ok_or_else(|| PyRuntimeError::new_err("expected primitivearray<u32>"))
-                .map(|pa| pa.clone().to_boxed())?,
+                .map(|pa| pa.clone().into_data())?,
         )
     } else {
         (
-            ListArray::from(griddiskdistances.cells).boxed(),
-            griddiskdistances.distances.boxed(),
+            GenericListArray::<i64>::from(griddiskdistances.cells).into_data(),
+            griddiskdistances.distances.into_data(),
         )
     };
 
     with_pyarrow(|py, pyarrow| {
-        let arrays = [
-            native_to_pyarray(cells, py, pyarrow)?,
-            native_to_pyarray(distances, py, pyarrow)?,
-        ];
+        let arrays = [cells.into_pyarrow(py)?, distances.into_pyarrow(py)?];
         let table = pyarrow
             .getattr("Table")?
             .call_method1("from_arrays", (arrays, [DEFAULT_CELL_COLUMN_NAME, "k"]))?;
@@ -115,8 +112,8 @@ pub(crate) fn grid_disk_aggregate_k(
 
     with_pyarrow(|py, pyarrow| {
         let arrays = [
-            h3array_to_pyarray(griddiskaggk.cells, py, pyarrow)?,
-            native_to_pyarray(griddiskaggk.distances.to_boxed(), py, pyarrow)?,
+            h3array_to_pyarray(griddiskaggk.cells, py)?,
+            griddiskaggk.distances.into_data().into_pyarrow(py)?,
         ];
         let table = pyarrow
             .getattr("Table")?
diff --git a/src/op/resolution.rs b/src/op/resolution.rs
index b404fc2..9695b2f 100644
--- a/src/op/resolution.rs
+++ b/src/op/resolution.rs
@@ -1,5 +1,6 @@
+use arrow::array::{Array, LargeListArray, PrimitiveArray};
+use arrow::pyarrow::{IntoPyArrow, ToPyArrow};
 use h3arrow::algorithm::ChangeResolutionOp;
-use h3arrow::export::arrow2::array::{ListArray, PrimitiveArray};
 use h3arrow::export::h3o::Resolution;
 use pyo3::prelude::*;
 
@@ -15,7 +16,7 @@ pub(crate) fn change_resolution(cellarray: &PyAny, h3_resolution: u8) -> PyResul
         .change_resolution(h3_resolution)
         .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(out, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(out, py))
 }
 
 #[pyfunction]
@@ -26,7 +27,7 @@ pub(crate) fn change_resolution_list(cellarray: &PyAny, h3_resolution: u8) -> Py
         .change_resolution_list(h3_resolution)
         .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| native_to_pyarray(ListArray::from(listarray).boxed(), py, pyarrow))
+    Python::with_gil(|py| LargeListArray::from(listarray).into_data().to_pyarrow(py))
 }
 
 #[pyfunction]
@@ -39,8 +40,8 @@ pub(crate) fn change_resolution_paired(cellarray: &PyAny, h3_resolution: u8) ->
 
     with_pyarrow(|py, pyarrow| {
         let arrays = [
-            h3array_to_pyarray(pair.before, py, pyarrow)?,
-            h3array_to_pyarray(pair.after, py, pyarrow)?,
+            h3array_to_pyarray(pair.before, py)?,
+            h3array_to_pyarray(pair.after, py)?,
         ];
         let table = pyarrow.getattr("Table")?.call_method1(
             "from_arrays",
@@ -59,7 +60,5 @@ pub(crate) fn change_resolution_paired(cellarray: &PyAny, h3_resolution: u8) ->
 #[pyfunction]
 pub(crate) fn cells_resolution(cellarray: &PyAny) -> PyResult<PyObject> {
     let resarray = pyarray_to_cellindexarray(cellarray)?.resolution();
-    with_pyarrow(|py, pyarrow| {
-        native_to_pyarray(PrimitiveArray::from(resarray).boxed(), py, pyarrow)
-    })
+    Python::with_gil(|py| PrimitiveArray::from(resarray).into_data().into_pyarrow(py))
 }
diff --git a/src/op/utf8.rs b/src/op/utf8.rs
index 10661d7..1dfa84a 100644
--- a/src/op/utf8.rs
+++ b/src/op/utf8.rs
@@ -1,6 +1,7 @@
-use h3arrow::algorithm::{ParseUtf8Array, ToUtf8Array};
+use arrow::array::{make_array, Array, ArrayData, LargeStringArray, StringArray};
+use arrow::pyarrow::{FromPyArrow, IntoPyArrow};
+use h3arrow::algorithm::{ParseGenericStringArray, ToGenericStringArray};
 use h3arrow::array::{CellIndexArray, DirectedEdgeIndexArray, VertexIndexArray};
-use h3arrow::export::arrow2::array::{Array, Utf8Array};
 use pyo3::exceptions::PyValueError;
 use pyo3::prelude::*;
 
@@ -10,18 +11,20 @@ use crate::error::IntoPyResult;
 #[pyfunction]
 #[pyo3(signature = (utf8array, set_failing_to_invalid = false))]
 pub(crate) fn cells_parse(utf8array: &PyAny, set_failing_to_invalid: bool) -> PyResult<PyObject> {
-    let boxed_array = pyarray_to_boxed(utf8array)?;
-    let cells = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i32>>() {
-        CellIndexArray::parse_utf8array(utf8array, set_failing_to_invalid).into_pyresult()?
-    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i64>>() {
-        CellIndexArray::parse_utf8array(utf8array, set_failing_to_invalid).into_pyresult()?
+    let boxed_array = make_array(ArrayData::from_pyarrow(utf8array)?);
+    let cells = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<StringArray>() {
+        CellIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
+            .into_pyresult()?
+    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<LargeStringArray>() {
+        CellIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
+            .into_pyresult()?
     } else {
         return Err(PyValueError::new_err(
             "unsupported array type to parse cells from",
         ));
     };
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(cells, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(cells, py))
 }
 
 #[pyfunction]
@@ -30,18 +33,20 @@ pub(crate) fn vertexes_parse(
     utf8array: &PyAny,
     set_failing_to_invalid: bool,
 ) -> PyResult<PyObject> {
-    let boxed_array = pyarray_to_boxed(utf8array)?;
-    let vertexes = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i32>>() {
-        VertexIndexArray::parse_utf8array(utf8array, set_failing_to_invalid).into_pyresult()?
-    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i64>>() {
-        VertexIndexArray::parse_utf8array(utf8array, set_failing_to_invalid).into_pyresult()?
+    let boxed_array = make_array(ArrayData::from_pyarrow(utf8array)?);
+    let vertexes = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<StringArray>() {
+        VertexIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
+            .into_pyresult()?
+    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<LargeStringArray>() {
+        VertexIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
+            .into_pyresult()?
     } else {
         return Err(PyValueError::new_err(
             "unsupported array type to parse vertexes from",
         ));
     };
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(vertexes, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(vertexes, py))
 }
 
 #[pyfunction]
@@ -50,12 +55,12 @@ pub(crate) fn directededges_parse(
     utf8array: &PyAny,
     set_failing_to_invalid: bool,
 ) -> PyResult<PyObject> {
-    let boxed_array = pyarray_to_boxed(utf8array)?;
-    let edges = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i32>>() {
-        DirectedEdgeIndexArray::parse_utf8array(utf8array, set_failing_to_invalid)
+    let boxed_array = make_array(pyarray_to_boxed(utf8array)?);
+    let edges = if let Some(utf8array) = boxed_array.as_any().downcast_ref::<StringArray>() {
+        DirectedEdgeIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
             .into_pyresult()?
-    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<Utf8Array<i64>>() {
-        DirectedEdgeIndexArray::parse_utf8array(utf8array, set_failing_to_invalid)
+    } else if let Some(utf8array) = boxed_array.as_any().downcast_ref::<LargeStringArray>() {
+        DirectedEdgeIndexArray::parse_genericstringarray(utf8array, set_failing_to_invalid)
             .into_pyresult()?
     } else {
         return Err(PyValueError::new_err(
@@ -63,35 +68,35 @@ pub(crate) fn directededges_parse(
         ));
     };
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(edges, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(edges, py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (cellarray))]
 pub(crate) fn cells_to_string(cellarray: &PyAny) -> PyResult<PyObject> {
-    let utf8array: Utf8Array<i64> = pyarray_to_cellindexarray(cellarray)?
-        .to_utf8array()
+    let stringarray: LargeStringArray = pyarray_to_cellindexarray(cellarray)?
+        .to_genericstringarray()
         .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| native_to_pyarray(utf8array.to_boxed(), py, pyarrow))
+    Python::with_gil(|py| stringarray.into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (vertexarray))]
 pub(crate) fn vertexes_to_string(vertexarray: &PyAny) -> PyResult<PyObject> {
-    let utf8array: Utf8Array<i64> = pyarray_to_vertexindexarray(vertexarray)?
-        .to_utf8array()
+    let stringarray: LargeStringArray = pyarray_to_vertexindexarray(vertexarray)?
+        .to_genericstringarray()
         .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| native_to_pyarray(utf8array.to_boxed(), py, pyarrow))
+    Python::with_gil(|py| stringarray.into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (directededgearray))]
 pub(crate) fn directededges_to_string(directededgearray: &PyAny) -> PyResult<PyObject> {
-    let utf8array: Utf8Array<i64> = pyarray_to_directededgeindexarray(directededgearray)?
-        .to_utf8array()
+    let stringarray: LargeStringArray = pyarray_to_directededgeindexarray(directededgearray)?
+        .to_genericstringarray()
         .into_pyresult()?;
 
-    with_pyarrow(|py, pyarrow| native_to_pyarray(utf8array.to_boxed(), py, pyarrow))
+    Python::with_gil(|py| stringarray.into_data().into_pyarrow(py))
 }
diff --git a/src/op/valid.rs b/src/op/valid.rs
index c9d8519..5048cf5 100644
--- a/src/op/valid.rs
+++ b/src/op/valid.rs
@@ -1,38 +1,33 @@
+use arrow::array::{Array, BooleanArray};
+use arrow::buffer::NullBuffer;
+use arrow::pyarrow::IntoPyArrow;
 use h3arrow::array::{FromIteratorWithValidity, H3Array, H3IndexArrayValue};
-use h3arrow::export::arrow2::array::{BooleanArray, PrimitiveArray};
-use h3arrow::export::arrow2::bitmap::{Bitmap, MutableBitmap};
-use h3arrow::export::arrow2::datatypes::DataType;
 use h3arrow::h3o;
 use h3o::{CellIndex, DirectedEdgeIndex, VertexIndex};
 use pyo3::prelude::*;
 
 use crate::arrow_interop::*;
-use crate::error::IntoPyResult;
 
 fn h3index_valid<IX>(arr: &PyAny, booleanarray: bool) -> PyResult<PyObject>
 where
     IX: H3IndexArrayValue,
 {
     let u64array = pyarray_to_uint64array(arr)?;
-    let validated = H3Array::<IX>::from_iter_with_validity(u64array.iter().map(|v| v.copied()));
+    let validated = H3Array::<IX>::from_iter_with_validity(u64array.iter());
 
-    with_pyarrow(|py, pyarrow| {
-        native_to_pyarray(
-            if booleanarray {
-                let pa: PrimitiveArray<_> = validated.into();
-                let bm: Bitmap = pa
-                    .validity()
-                    .cloned()
-                    .unwrap_or_else(|| MutableBitmap::from_len_set(pa.len()).into());
-                BooleanArray::try_new(DataType::Boolean, bm, None)
-                    .into_pyresult()?
-                    .boxed()
-            } else {
-                PrimitiveArray::from(validated).boxed()
-            },
-            py,
-            pyarrow,
-        )
+    Python::with_gil(|py| {
+        if booleanarray {
+            let nullbuffer = validated
+                .primitive_array()
+                .nulls()
+                .cloned()
+                .unwrap_or_else(|| NullBuffer::new_valid(validated.len()));
+            BooleanArray::from(nullbuffer.into_inner())
+                .into_data()
+                .into_pyarrow(py)
+        } else {
+            h3array_to_pyarray(validated, py)
+        }
     })
 }
 
diff --git a/src/raster.rs b/src/raster.rs
index 3912804..c56fd61 100644
--- a/src/raster.rs
+++ b/src/raster.rs
@@ -3,8 +3,12 @@ use std::hash::Hash;
 use std::iter::repeat;
 use std::str::FromStr;
 
+use arrow::array::{
+    Array, Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, UInt16Array,
+    UInt32Array, UInt64Array, UInt8Array,
+};
+use arrow::pyarrow::IntoPyArrow;
 use h3arrow::array::CellIndexArray;
-use h3arrow::export::arrow2::array::PrimitiveArray;
 use h3arrow::export::h3o::{CellIndex, Resolution};
 use ndarray::ArrayView2;
 use numpy::PyReadonlyArray2;
@@ -12,7 +16,7 @@ use ordered_float::OrderedFloat;
 use pyo3::exceptions::PyValueError;
 use pyo3::{prelude::*, wrap_pyfunction, PyNativeType};
 
-use crate::arrow_interop::{h3array_to_pyarray, native_to_pyarray, with_pyarrow};
+use crate::arrow_interop::h3array_to_pyarray;
 use crate::error::IntoPyResult;
 use crate::transform::Transform;
 
@@ -134,7 +138,7 @@ where
 }
 
 macro_rules! make_raster_to_h3_variant {
-    ($name:ident, $dtype:ty) => {
+    ($name:ident, $dtype:ty, $array_dtype:ty) => {
         #[pyfunction]
         fn $name(
             np_array: PyReadonlyArray2<$dtype>,
@@ -154,11 +158,9 @@ macro_rules! make_raster_to_h3_variant {
                 compact,
             )?;
 
-            with_pyarrow(|py, pyarrow| {
-                let values = PrimitiveArray::from_vec(values);
-                let cells = h3array_to_pyarray(CellIndexArray::from(cells), py, pyarrow)?;
-
-                let values = native_to_pyarray(values.boxed(), py, pyarrow)?;
+            Python::with_gil(|py| {
+                let values = <$array_dtype>::from(values).into_data().into_pyarrow(py)?;
+                let cells = h3array_to_pyarray(CellIndexArray::from(cells), py)?;
 
                 Ok((values, cells))
             })
@@ -167,7 +169,7 @@ macro_rules! make_raster_to_h3_variant {
 }
 
 macro_rules! make_raster_to_h3_float_variant {
-    ($name:ident, $dtype:ty) => {
+    ($name:ident, $dtype:ty, $array_dtype:ty) => {
         #[pyfunction]
         fn $name(
             np_array: PyReadonlyArray2<$dtype>,
@@ -190,13 +192,10 @@ macro_rules! make_raster_to_h3_float_variant {
                 compact,
             )?;
 
-            with_pyarrow(|py, pyarrow| {
-                let values = PrimitiveArray::<$dtype>::from_vec(
-                    values.into_iter().map(|v| v.into_inner()).collect(),
-                );
-                let cells = h3array_to_pyarray(CellIndexArray::from(cells), py, pyarrow)?;
-
-                let values = native_to_pyarray(values.boxed(), py, pyarrow)?;
+            Python::with_gil(|py| {
+                let values: Vec<$dtype> = values.into_iter().map(|v| v.into_inner()).collect();
+                let values = <$array_dtype>::from(values).into_data().into_pyarrow(py)?;
+                let cells = h3array_to_pyarray(CellIndexArray::from(cells), py)?;
 
                 Ok((values, cells))
             })
@@ -205,16 +204,16 @@ macro_rules! make_raster_to_h3_float_variant {
 }
 
 // generate some specialized variants of raster_to_h3 to expose to python
-make_raster_to_h3_variant!(raster_to_h3_u8, u8);
-make_raster_to_h3_variant!(raster_to_h3_i8, i8);
-make_raster_to_h3_variant!(raster_to_h3_u16, u16);
-make_raster_to_h3_variant!(raster_to_h3_i16, i16);
-make_raster_to_h3_variant!(raster_to_h3_u32, u32);
-make_raster_to_h3_variant!(raster_to_h3_i32, i32);
-make_raster_to_h3_variant!(raster_to_h3_u64, u64);
-make_raster_to_h3_variant!(raster_to_h3_i64, i64);
-make_raster_to_h3_float_variant!(raster_to_h3_f32, f32);
-make_raster_to_h3_float_variant!(raster_to_h3_f64, f64);
+make_raster_to_h3_variant!(raster_to_h3_u8, u8, UInt8Array);
+make_raster_to_h3_variant!(raster_to_h3_i8, i8, Int8Array);
+make_raster_to_h3_variant!(raster_to_h3_u16, u16, UInt16Array);
+make_raster_to_h3_variant!(raster_to_h3_i16, i16, Int16Array);
+make_raster_to_h3_variant!(raster_to_h3_u32, u32, UInt32Array);
+make_raster_to_h3_variant!(raster_to_h3_i32, i32, Int32Array);
+make_raster_to_h3_variant!(raster_to_h3_u64, u64, UInt64Array);
+make_raster_to_h3_variant!(raster_to_h3_i64, i64, Int64Array);
+make_raster_to_h3_float_variant!(raster_to_h3_f32, f32, Float32Array);
+make_raster_to_h3_float_variant!(raster_to_h3_f64, f64, Float64Array);
 
 pub fn init_raster_submodule(m: &PyModule) -> PyResult<()> {
     m.add("Transform", m.py().get_type::<Transform>())?;
diff --git a/src/vector.rs b/src/vector.rs
index 719336f..40f1c57 100644
--- a/src/vector.rs
+++ b/src/vector.rs
@@ -1,11 +1,12 @@
-use geo::BoundingRect;
+use arrow::array::{Array, Float64Array, LargeBinaryArray, LargeListArray, UInt8Array};
+use arrow::buffer::NullBuffer;
+use arrow::pyarrow::{IntoPyArrow, ToPyArrow};
+use geo::{BoundingRect, HasDimensions};
 use h3arrow::algorithm::ToCoordinatesOp;
 use h3arrow::array::from_geo::{ToCellIndexArray, ToCellListArray, ToCellsOptions};
-use h3arrow::array::to_geoarrow::{ToWKBLineStrings, ToWKBLines, ToWKBPoints, ToWKBPolygons};
+use h3arrow::array::to_geoarrow::{ToWKBLineStrings, ToWKBPoints, ToWKBPolygons};
 use h3arrow::array::{CellIndexArray, ResolutionArray};
-use h3arrow::export::arrow2::array::{BinaryArray, Float64Array, ListArray, UInt8Array};
-use h3arrow::export::arrow2::bitmap::Bitmap;
-use h3arrow::export::geoarrow::{array::WKBArray, GeometryArrayTrait};
+use h3arrow::export::geoarrow::array::{WKBArray, WKBBuilder, WKBCapacity};
 use h3arrow::export::h3o::geom::{ContainmentMode, ToGeo};
 use h3arrow::export::h3o::Resolution;
 use h3arrow::h3o::geom::PolyfillConfig;
@@ -46,12 +47,16 @@ use crate::error::{warn_deprecated, IntoPyResult};
 ///         belong to two different polygons if they are adjacent/close enough. Some
 ///         cells may cover area outside of the polygon.
 ///
+/// * Covers: This mode behaves the same as IntersectsBoundary, but also handles the case where the geometry is
+///         being covered by a cell without intersecting with its boundaries. In such cases, the covering cell is returned.
+///
 #[pyclass(name = "ContainmentMode")]
 #[derive(Copy, Clone, Eq, PartialEq)]
 pub enum PyContainmentMode {
     ContainsCentroid,
     ContainsBoundary,
     IntersectsBoundary,
+    Covers,
 }
 
 impl PyContainmentMode {
@@ -60,6 +65,7 @@ impl PyContainmentMode {
             PyContainmentMode::ContainsCentroid => ContainmentMode::ContainsCentroid,
             PyContainmentMode::ContainsBoundary => ContainmentMode::ContainsBoundary,
             PyContainmentMode::IntersectsBoundary => ContainmentMode::IntersectsBoundary,
+            PyContainmentMode::Covers => ContainmentMode::Covers,
         }
     }
 }
@@ -113,38 +119,22 @@ pub(crate) fn cells_bounds_arrays(cellarray: &PyAny) -> PyResult<PyObject> {
         }
     }
 
-    let validity_bm = Bitmap::from(validity_vec.as_slice());
+    let validity = NullBuffer::from(validity_vec);
 
     with_pyarrow(|py, pyarrow| {
         let arrays = [
-            native_to_pyarray(
-                Float64Array::from_vec(minx_vec)
-                    .with_validity(Some(validity_bm.clone()))
-                    .boxed(),
-                py,
-                pyarrow,
-            )?,
-            native_to_pyarray(
-                Float64Array::from_vec(miny_vec)
-                    .with_validity(Some(validity_bm.clone()))
-                    .boxed(),
-                py,
-                pyarrow,
-            )?,
-            native_to_pyarray(
-                Float64Array::from_vec(maxx_vec)
-                    .with_validity(Some(validity_bm.clone()))
-                    .boxed(),
-                py,
-                pyarrow,
-            )?,
-            native_to_pyarray(
-                Float64Array::from_vec(maxy_vec)
-                    .with_validity(Some(validity_bm.clone()))
-                    .boxed(),
-                py,
-                pyarrow,
-            )?,
+            Float64Array::new(minx_vec.into(), Some(validity.clone()))
+                .into_data()
+                .into_pyarrow(py)?,
+            Float64Array::new(miny_vec.into(), Some(validity.clone()))
+                .into_data()
+                .into_pyarrow(py)?,
+            Float64Array::new(maxx_vec.into(), Some(validity.clone()))
+                .into_data()
+                .into_pyarrow(py)?,
+            Float64Array::new(maxy_vec.into(), Some(validity))
+                .into_data()
+                .into_pyarrow(py)?,
         ];
         let table = pyarrow
             .getattr("Table")?
@@ -167,8 +157,8 @@ pub(crate) fn cells_to_coordinates(cellarray: &PyAny, radians: bool) -> PyResult
 
     with_pyarrow(|py, pyarrow| {
         let arrays = [
-            native_to_pyarray(coordinate_arrays.lat.boxed(), py, pyarrow)?,
-            native_to_pyarray(coordinate_arrays.lng.boxed(), py, pyarrow)?,
+            coordinate_arrays.lat.into_data().into_pyarrow(py)?,
+            coordinate_arrays.lng.into_data().into_pyarrow(py)?,
         ];
         let table = pyarrow
             .getattr("Table")?
@@ -202,9 +192,9 @@ pub(crate) fn coordinates_to_cells(
             .map(|(lat, lng)| {
                 if let (Some(lat), Some(lng)) = (lat, lng) {
                     if radians {
-                        LatLng::from_radians(*lat, *lng).into_pyresult()
+                        LatLng::from_radians(lat, lng).into_pyresult()
                     } else {
-                        LatLng::new(*lat, *lng).into_pyresult()
+                        LatLng::new(lat, lng).into_pyresult()
                     }
                     .map(|ll| Some(ll.to_cell(resolution)))
                 } else {
@@ -226,9 +216,9 @@ pub(crate) fn coordinates_to_cells(
             .map(|(lat, lng, res)| {
                 if let (Some(lat), Some(lng), Some(res)) = (lat, lng, res) {
                     if radians {
-                        LatLng::from_radians(*lat, *lng).into_pyresult()
+                        LatLng::from_radians(lat, lng).into_pyresult()
                     } else {
-                        LatLng::new(*lat, *lng).into_pyresult()
+                        LatLng::new(lat, lng).into_pyresult()
                     }
                     .map(|ll| Some(ll.to_cell(res)))
                 } else {
@@ -238,7 +228,7 @@ pub(crate) fn coordinates_to_cells(
             .collect::<PyResult<CellIndexArray>>()?
     };
 
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(cells, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(cells, py))
 }
 
 #[pyfunction]
@@ -251,69 +241,58 @@ pub(crate) fn cells_to_wkb_polygons(
     let cellindexarray = pyarray_to_cellindexarray(cellarray)?;
     let use_degrees = !radians;
 
-    let cells = if link_cells {
-        WKBArray::from(
-            cellindexarray
-                .iter()
-                .flatten()
-                .to_geom(use_degrees)
-                .into_pyresult()?
-                .0
-                .into_iter()
-                .map(|poly| Some(geo_types::Geometry::from(poly)))
-                .collect::<Vec<_>>(),
-        )
+    let out: WKBArray<i64> = if link_cells {
+        let geoms = cellindexarray
+            .iter()
+            .flatten()
+            .to_geom(use_degrees)
+            .into_pyresult()?
+            .0
+            .into_iter()
+            .map(|poly| Some(geo_types::Geometry::from(poly)))
+            .collect::<Vec<_>>();
+        let mut builder = WKBBuilder::with_capacity(WKBCapacity::from_geometries(
+            geoms.iter().map(|v| v.as_ref()),
+        ));
+        builder.extend_from_iter(geoms.iter().map(|v| v.as_ref()));
+        builder.finish()
     } else {
-        cellindexarray.to_wkb_polygons(use_degrees).unwrap()
-    }
-    .into_arrow()
-    .boxed();
+        cellindexarray
+            .to_wkb_polygons(use_degrees)
+            .expect("wkbarray")
+    };
 
-    with_pyarrow(|py, pyarrow| native_to_pyarray(cells, py, pyarrow))
+    Python::with_gil(|py| out.into_inner().into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (cellarray, radians = false))]
 pub(crate) fn cells_to_wkb_points(cellarray: &PyAny, radians: bool) -> PyResult<PyObject> {
-    let cellindexarray = pyarray_to_cellindexarray(cellarray)?;
-    let out = cellindexarray
-        .to_wkb_points(!radians)
-        .unwrap()
-        .into_arrow()
-        .boxed();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out, py, pyarrow))
+    let out = pyarray_to_cellindexarray(cellarray)?
+        .to_wkb_points::<i64>(!radians)
+        .expect("wkbarray");
+
+    Python::with_gil(|py| out.into_inner().into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (vertexarray, radians = false))]
 pub(crate) fn vertexes_to_wkb_points(vertexarray: &PyAny, radians: bool) -> PyResult<PyObject> {
-    let vertexindexarray = pyarray_to_vertexindexarray(vertexarray)?;
-    let out = vertexindexarray
-        .to_wkb_points(!radians)
-        .unwrap()
-        .into_arrow()
-        .boxed();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out, py, pyarrow))
-}
+    let out = pyarray_to_vertexindexarray(vertexarray)?
+        .to_wkb_points::<i64>(!radians)
+        .expect("wkbarray");
 
-#[pyfunction]
-#[pyo3(signature = (array, radians = false))]
-pub(crate) fn directededges_to_wkb_lines(array: &PyAny, radians: bool) -> PyResult<PyObject> {
-    let array = pyarray_to_directededgeindexarray(array)?;
-    let out = array.to_wkb_lines(!radians).unwrap().into_arrow().boxed();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out, py, pyarrow))
+    Python::with_gil(|py| out.into_inner().into_data().into_pyarrow(py))
 }
 
 #[pyfunction]
 #[pyo3(signature = (array, radians = false))]
 pub(crate) fn directededges_to_wkb_linestrings(array: &PyAny, radians: bool) -> PyResult<PyObject> {
-    let array = pyarray_to_directededgeindexarray(array)?;
-    let out = array
-        .to_wkb_linestrings(!radians)
-        .unwrap()
-        .into_arrow()
-        .boxed();
-    with_pyarrow(|py, pyarrow| native_to_pyarray(out, py, pyarrow))
+    let out = pyarray_to_directededgeindexarray(array)?
+        .to_wkb_linestrings::<i64>(!radians)
+        .expect("wkbarray");
+
+    Python::with_gil(|py| out.into_inner().into_data().into_pyarrow(py))
 }
 
 fn get_containment_mode(
@@ -356,15 +335,18 @@ pub(crate) fn wkb_to_cells(
     flatten: bool,
 ) -> PyResult<PyObject> {
     let options = get_to_cells_options(resolution, containment_mode, all_intersecting, compact)?;
-    let wkbarray = WKBArray::new(pyarray_to_native::<BinaryArray<i64>>(array)?);
+    let wkbarray = WKBArray::new(
+        pyarray_to_native::<LargeBinaryArray>(array)?,
+        Default::default(),
+    ); // TODO: handle BinaryArray i32
 
     if flatten {
         let cells = wkbarray.to_cellindexarray(&options).into_pyresult()?;
 
-        with_pyarrow(|py, pyarrow| h3array_to_pyarray(cells, py, pyarrow))
+        Python::with_gil(|py| h3array_to_pyarray(cells, py))
     } else {
-        let listarray: ListArray<_> = wkbarray.to_celllistarray(&options).into_pyresult()?.into();
-        with_pyarrow(|py, pyarrow| native_to_pyarray(listarray.boxed(), py, pyarrow))
+        let listarray: LargeListArray = wkbarray.to_celllistarray(&options).into_pyresult()?.into();
+        Python::with_gil(|py| listarray.into_data().to_pyarrow(py))
     }
 }
 
@@ -377,11 +359,14 @@ pub(crate) fn geometry_to_cells(
     compact: bool,
     all_intersecting: Option<bool>,
 ) -> PyResult<PyObject> {
+    if obj.0.is_empty() {
+        return Python::with_gil(|py| h3array_to_pyarray(CellIndexArray::new_null(0), py));
+    }
     let options = get_to_cells_options(resolution, containment_mode, all_intersecting, compact)?;
     let cellindexarray = CellIndexArray::from(
         h3arrow::array::from_geo::geometry_to_cells(&obj.0, &options).into_pyresult()?,
     );
-    with_pyarrow(|py, pyarrow| h3array_to_pyarray(cellindexarray, py, pyarrow))
+    Python::with_gil(|py| h3array_to_pyarray(cellindexarray, py))
 }
 
 pub fn init_vector_submodule(m: &PyModule) -> PyResult<()> {
@@ -392,7 +377,6 @@ pub fn init_vector_submodule(m: &PyModule) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(cells_to_wkb_points, m)?)?;
     m.add_function(wrap_pyfunction!(vertexes_to_wkb_points, m)?)?;
     m.add_function(wrap_pyfunction!(directededges_to_wkb_linestrings, m)?)?;
-    m.add_function(wrap_pyfunction!(directededges_to_wkb_lines, m)?)?;
     m.add_function(wrap_pyfunction!(wkb_to_cells, m)?)?;
     m.add_function(wrap_pyfunction!(geometry_to_cells, m)?)?;
     m.add_function(wrap_pyfunction!(coordinates_to_cells, m)?)?;


Test patch:
diff --git a/tests/pandas/test_vector.py b/tests/pandas/test_vector.py
index cec0d39..4068f7c 100644
--- a/tests/pandas/test_vector.py
+++ b/tests/pandas/test_vector.py
@@ -13,7 +13,7 @@
 )
 from h3ronpy import DEFAULT_CELL_COLUMN_NAME, ContainmentMode
 import geopandas as gpd
-from .. import load_africa
+from .. import load_africa, TESTDATA_PATH
 
 
 def test_cells_to_points():
@@ -85,6 +85,7 @@ def test_geoseries_to_cells_flatten():
     assert cells.dtype == "uint64"
 
 
+@pytest.mark.skip(reason="GeometryCollections are unsupported until https://github.com/geoarrow/geoarrow-rs/blob/3a2aaa883126274037cabaf46b1f5f6459938297/src/io/wkb/reader/geometry_collection.rs#L23 is fixed")
 def test_empty_geometrycollection_omitted():
     gdf = gpd.GeoDataFrame(
         {