Repository: geopandas/geopandas
Pull request #2900 (2023-05-22 10:35:05)

ENH: dwithin predicate and distance argument for query and sjoin
This is my first pull request and a response to #2882, feedback very welcome. I have incorporated the ``predicate="dwithin"`` and `distance` arugment into sindex, also for rtree structures, and written corresponding tests into test_sindex.py and test_geodataframe. I'm not 100% sure about version compatibility for 'dwithin' but have specified either:

USE_SHAPELY_20 or 
USE_PYGEOS and PyGEOS >=0.12.0 and GEOS >= 3.10.0

since dwithin requires GEOS 3.10.0 and PyGEOS only [added this feature in 0.12.0 ](https://pygeos.readthedocs.io/en/latest/changelog.html)

Modification of __compat.py was necessary to track these version requirements.

Docstring for query in sindex has been updated to include 'dwithin' in predicates and to include examples using 'dwithin'.

NB: All tests passed when using shapely. Using PyGEOS, 9 failed tests are related to pandas version and not the enhancement, the 9 failed tests with PyGEOS also occur in the main branch.

chris-hedemann: The codecov test is failing due to error handling for outdated versions of pyGEOS and Shapely that aren't used in the tests. I could remove the dwithin functionality for rtree data types... Can someone please help me over the finish line?
chris-hedemann: @brendan-ward thanks for the feedback! I've implemented everything. I'm still failing on the codecov/patch test since `test_dwithin_requirements()` is never tested. Everything else seems OK.
chris-hedemann: @brendan-ward thanks for the finishing touches! I tried to apply the same concepts to my other edits as well. I also realised that I didn't build in the array-like capability for the distance parameter in Rtree. Might as well, since we were already 90% of the way there for enabling dwithin for Rtree geometries. I hope everything is up to scratch now! 
chris-hedemann: Ah I see, thanks! I've now adapted the code from shapely, but with some differences: 

- making geometry into an array only happens for Rtree geometries when predicate = "dwithin". This is because the minimal build (no PyGEOS, shapely=1.7.1) was doing odd things with geometries like `MultiPolygon` and  `LineString` when trying to convert them to arrays. The minimal build cannot use "dwithin" and a clash is avoided.
- if the geometry is scalar, it's reverted to its original form as a non-array, to keep the original conditions for looping: 
```python
if hasattr(geometry, "__array__") and not isinstance(
    geometry, BaseGeometry
):
    # Iterates over geometry, applying query
``` 
This means that geometries passed as lists still fail unless predicate="dwithin", but it preserves the original code while solving the broadcasting issue for "dwithin". I hope that fixes the issue.
m-richards: Hi @chris-hedemann, I've just merged this back up to date with main. There were a decent amount of conflicts as the next release will require shapely 2.0 (please let me know if something doesn't look right!). Unfortunately this means that Rtree is also no longer a dependency, and so the parts of this pull request to add rtree support are no longer needed (appologies for the long timescales between this being drafted, first review and then merging). 

Whilst joris is probably better placed to review, I'd like to try and move this forward again, so I will aim to do a review myself in the next little while.
jorisvandenbossche: @chris-hedemann again apologies for our slow review cycle .. I merged with the latest main branch and fixed the test for the changed error message.

I think we still need to update the sjoin() docstring to include an entry for the new distance parameter. And I think we might also need some actual test for sjoin in `test_sjoin.py` (currently it's only tested for the sindex.query() method). 


chris-hedemann: @jorisvandenbossche yes, absolutely. My time resources have significantly reduced since the PR last May and the learning curve for getting back in is steeper than I thought. I'm currently unsure whether the distance parameter can be an array in the sjoin. It can in the query. I'll have a look on the weekend and try to solve this. If you already know the answer, let me know, that would save me some time.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 191af6eb..fadd1dd8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,8 @@ New methods:
 
 New features and improvements:
 
+- Added ``predicate="dwithin"`` option and ``distance`` argument to the ``sindex.query()`` method
+ and ``sjoin`` (#2882).
 - GeoSeries and GeoDataFrame `__repr__` now trims trailing zeros for a more readable
   output (#3087).
 - Add `on_invalid` parameter to `from_wkt` and `from_wkb` (#3110).
@@ -47,6 +49,12 @@ Bug fixes:
 - Fix bug in `pandas.concat` CRS consistency checking where CRS differing by WKT
   whitespace only were treated as incompatible (#3023).
 
+Deprecations and compatibility notes:
+
+- The deprecation of `geopandas.datasets` has been enforced and the module has been 
+  removed. New sample datasets are now available in the 
+  [geodatasets](https://geodatasets.readthedocs.io/en/latest/) package (#3084). 
+
 ## Version 0.14.2 (Jan 4, 2024)
 
 - Fix regression in `overlay` where using `buffer(0)` instead of `make_valid` internally
@@ -87,6 +95,7 @@ New methods:
 New features and improvements:
 
 - Added ``exclusive`` parameter to ``sjoin_nearest`` method for Shapely >= 2.0 (#2877)
+- Added ``GeoDataFrame.active_geometry_name`` property returning the active geometry column's name or None if no active geometry column is set.
 - The ``to_file()`` method will now automatically detect the FlatGeoBuf driver
   for files with the `.fgb` extension (#2958)
 
diff --git a/benchmarks/clip.py b/benchmarks/clip.py
index 235d994f..8e3d9573 100644
--- a/benchmarks/clip.py
+++ b/benchmarks/clip.py
@@ -1,11 +1,21 @@
-from geopandas import read_file, datasets, clip
+from geopandas import read_file, clip
 from shapely.geometry import box
 
+try:
+    from geopandas.tests.util import _NATURALEARTH_LOWRES
+    from geopandas.tests.util import _NATURALEARTH_CITIES
+except:
+    # geopandas <=0.14 compat
+    from geopandas.datasets import get_path
+
+    _NATURALEARTH_LOWRES = get_path("naturalearth_lowres")
+    _NATURALEARTH_CITIES = get_path("naturalearth_cities")
+
 
 class Bench:
     def setup(self, *args):
-        world = read_file(datasets.get_path("naturalearth_lowres"))
-        capitals = read_file(datasets.get_path("naturalearth_cities"))
+        world = read_file(_NATURALEARTH_LOWRES)
+        capitals = read_file(_NATURALEARTH_CITIES)
         self.bounds = [box(*geom.bounds) for geom in world.geometry]
         self.points = capitals
 
diff --git a/benchmarks/overlay.py b/benchmarks/overlay.py
index cd352e59..47778cb4 100644
--- a/benchmarks/overlay.py
+++ b/benchmarks/overlay.py
@@ -1,7 +1,8 @@
-from geopandas import GeoDataFrame, GeoSeries, read_file, datasets, overlay
+from geopandas import GeoDataFrame, GeoSeries, read_file, overlay
 import numpy as np
 from shapely.geometry import Point, Polygon
-
+from geopandas.tests.util import _NATURALEARTH_LOWRES
+from geopandas.tests.util import _NATURALEARTH_CITIES
 
 class Countries:
     param_names = ["how"]
@@ -10,8 +11,8 @@ class Countries:
     ]
 
     def setup(self, *args):
-        world = read_file(datasets.get_path("naturalearth_lowres"))
-        capitals = read_file(datasets.get_path("naturalearth_cities"))
+        world = read_file(_NATURALEARTH_LOWRES)
+        capitals = read_file(_NATURALEARTH_CITIES)
         countries = world[["geometry", "name"]]
         countries = countries.to_crs("+init=epsg:3395")[countries.name != "Antarctica"]
         capitals = capitals.to_crs("+init=epsg:3395")
diff --git a/benchmarks/sindex.py b/benchmarks/sindex.py
index bbd15a62..a812c02f 100644
--- a/benchmarks/sindex.py
+++ b/benchmarks/sindex.py
@@ -1,6 +1,7 @@
 from shapely.geometry import Point
-
-from geopandas import read_file, datasets, GeoSeries
+from geopandas.tests.util import _NATURALEARTH_LOWRES
+from geopandas.tests.util import _NATURALEARTH_CITIES
+from geopandas import read_file, GeoSeries
 
 
 # Derive list of valid query predicates based on underlying index backend;
@@ -12,8 +13,8 @@ geom_types = ("mixed", "points", "polygons")
 
 
 def generate_test_df():
-    world = read_file(datasets.get_path("naturalearth_lowres"))
-    capitals = read_file(datasets.get_path("naturalearth_cities"))
+    world = read_file(_NATURALEARTH_LOWRES)
+    capitals = read_file(_NATURALEARTH_CITIES)
     countries = world.to_crs("epsg:3395")[["geometry"]]
     capitals = capitals.to_crs("epsg:3395")[["geometry"]]
     mixed = capitals.append(countries)  # get a mix of geometries
diff --git a/benchmarks/transform.py b/benchmarks/transform.py
index 673d1786..789335fd 100644
--- a/benchmarks/transform.py
+++ b/benchmarks/transform.py
@@ -1,12 +1,12 @@
-from geopandas import GeoDataFrame, GeoSeries, read_file, datasets
+from geopandas import GeoDataFrame, GeoSeries, read_file
 import numpy as np
 import pandas as pd
 from shapely.geometry import Point
-
+from geopandas.tests.util import _NYBB
 
 class CRS:
     def setup(self):
-        nybb = read_file(datasets.get_path("nybb"))
+        nybb = read_file(_NYBB)
         self.long_nybb = GeoDataFrame(pd.concat(10 * [nybb]), crs=nybb.crs)
 
         num_points = 20000
diff --git a/doc/source/docs/reference/geodataframe.rst b/doc/source/docs/reference/geodataframe.rst
index dc49c40d..d6a8090a 100644
--- a/doc/source/docs/reference/geodataframe.rst
+++ b/doc/source/docs/reference/geodataframe.rst
@@ -49,6 +49,7 @@ Active geometry handling
 
    GeoDataFrame.rename_geometry
    GeoDataFrame.set_geometry
+   GeoDataFrame.active_geometry_name
 
 Aggregating and exploding
 -------------------------
diff --git a/geopandas/_compat.py b/geopandas/_compat.py
index 4f28d1f0..f254bd64 100644
--- a/geopandas/_compat.py
+++ b/geopandas/_compat.py
@@ -23,6 +23,7 @@ PANDAS_GE_22 = Version(pd.__version__) >= Version("2.2.0.dev0")
 
 
 GEOS_GE_390 = shapely.geos.geos_version >= (3, 9, 0)
+GEOS_GE_310 = shapely.geos.geos_version >= (3, 10, 0)
 
 
 def import_optional_dependency(name: str, extra: str = ""):
diff --git a/geopandas/datasets/__init__.py b/geopandas/datasets/__init__.py
index bed996ad..c0c03050 100644
--- a/geopandas/datasets/__init__.py
+++ b/geopandas/datasets/__init__.py
@@ -1,59 +1,25 @@
-import os
-
-from warnings import warn
-
-__all__ = ["available", "get_path"]
-
-_module_path = os.path.dirname(__file__)
-_available_dir = [p for p in next(os.walk(_module_path))[1] if not p.startswith("__")]
-_available_zip = {"nybb": "nybb_16a.zip"}
-available = _available_dir + list(_available_zip.keys())
+__all__ = []
+available = []  # previously part of __all__
+_prev_available = ["naturalearth_cities", "naturalearth_lowres", "nybb"]
 
 
 def get_path(dataset):
-    """
-    Get the path to the data file.
-
-    Parameters
-    ----------
-    dataset : str
-        The name of the dataset. See ``geopandas.datasets.available`` for
-        all options.
-
-    Examples
-    --------
-    >>> geopandas.datasets.get_path("naturalearth_lowres")  # doctest: +SKIP
-    '.../python3.8/site-packages/geopandas/datasets/\
-naturalearth_lowres/naturalearth_lowres.shp'
-
-    """
     ne_message = "https://www.naturalearthdata.com/downloads/110m-cultural-vectors/."
     nybb_message = (
         "the geodatasets package.\n\nfrom geodatasets import get_path\n"
         "path_to_file = get_path('nybb')\n"
     )
-    depr_warning = (
-        "The geopandas.dataset module is deprecated and will be removed in GeoPandas "
+    error_msg = (
+        "The geopandas.dataset has been deprecated and was removed in GeoPandas "
         f"1.0. You can get the original '{dataset}' data from "
         f"{ne_message if 'natural' in dataset else nybb_message}"
     )
-
-    if dataset in _available_dir:
-        warn(
-            depr_warning,
-            FutureWarning,
-            stacklevel=2,
-        )
-        return os.path.abspath(os.path.join(_module_path, dataset, dataset + ".shp"))
-    elif dataset in _available_zip:
-        warn(
-            depr_warning,
-            FutureWarning,
-            stacklevel=2,
-        )
-        fpath = os.path.abspath(os.path.join(_module_path, _available_zip[dataset]))
-        return "zip://" + fpath
+    if dataset in _prev_available:
+        raise AttributeError(error_msg)
     else:
-        msg = "The dataset '{data}' is not available. ".format(data=dataset)
-        msg += "Available datasets are {}".format(", ".join(available))
-        raise ValueError(msg)
+        error_msg = (
+            "The geopandas.dataset has been deprecated and "
+            "was removed in GeoPandas 1.0. New sample datasets are now available "
+            "in the geodatasets package (https://geodatasets.readthedocs.io/en/latest/)"
+        )
+        raise AttributeError(error_msg)
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html b/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html
deleted file mode 100644
index 2f5786f0..00000000
--- a/geopandas/datasets/naturalearth_cities/naturalearth_cities.README.html
+++ /dev/null
@@ -1,336 +0,0 @@
-
-
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
-
-<head profile="http://gmpg.org/xfn/11">
-<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-
-<title>Populated Places | Natural Earth</title>
-
-<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
-<link rel="alternate" type="application/rss+xml" title="Natural Earth RSS Feed" href="http://www.naturalearthdata.com/feed/" />
-<link rel="pingback" href="http://www.naturalearthdata.com/xmlrpc.php" />
-<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/themes/NEV/includes/js/suckerfish.js"></script>
-<!--[if lt IE 7]>
-    <script src="http://ie7-js.googlecode.com/svn/version/2.0(beta3)/IE7.js" type="text/javascript"></script>
-    <script defer="defer" type="text/javascript" src="http://www.naturalearthdata.com/wp-content/themes/NEV/includes/js/pngfix.js"></script>
-<![endif]--> 
-<link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/themes/NEV/style.css" type="text/css" media="screen" />
-
-<meta name='Admin Management Xtended WordPress plugin' content='2.1.1' />
-<link rel="alternate" type="application/rss+xml" title="Natural Earth &raquo; Populated Places Comments Feed" href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/feed/" />
-<link rel='stylesheet' id='sociable-front-css-css'  href='http://www.naturalearthdata.com/wp-content/plugins/sociable/sociable.css?ver=2.9.2' type='text/css' media='' />
-<link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://www.naturalearthdata.com/xmlrpc.php?rsd" />
-<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://www.naturalearthdata.com/wp-includes/wlwmanifest.xml" /> 
-<link rel='index' title='Natural Earth' href='http://www.naturalearthdata.com' />
-<link rel='start' title='Welcome to the Natural Earth Blog' href='http://www.naturalearthdata.com/blog/miscellaneous/test/' />
-<link rel='prev' title='Antarctic Ice Shelves' href='http://www.naturalearthdata.com/downloads/10m-physical-vectors/10m-antarctic-ice-shelves/' />
-<link rel='next' title='Admin 1 &#8211; States, Provinces' href='http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-admin-1-states-provinces/' />
-<meta name="generator" content="WordPress 2.9.2" />
-
-<!-- All in One SEO Pack 1.6.10.2 by Michael Torbert of Semper Fi Web Design[309,448] -->
-<meta name="description" content="City and town points, from Tokyo to Wasilla, Cairo to Kandahar About Point symbols with name attributes. Includes all admin-0 and many" />
-<link rel="canonical" href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/" />
-<!-- /all in one seo pack -->
-
-	<!-- begin gallery scripts -->
-    <link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/css/jd.gallery.css.php" type="text/css" media="screen" charset="utf-8"/>
-	<link rel="stylesheet" href="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/css/jd.gallery.css" type="text/css" media="screen" charset="utf-8"/>
-	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/mootools.v1.11.js"></script>
-	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/jd.gallery.js.php"></script>
-	<script type="text/javascript" src="http://www.naturalearthdata.com/wp-content/plugins/featured-content-gallery/scripts/jd.gallery.transitions.js"></script>
-	<!-- end gallery scripts -->
-<style type="text/css">.broken_link, a.broken_link {
-	text-decoration: line-through;
-}</style><link href="http://www.naturalearthdata.com/wp-content/themes/NEV/css/default.css" rel="stylesheet" type="text/css" />
-	<style type="text/css">.recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;}</style>
-<!--[if lte IE 7]>
-<link rel="stylesheet" type="text/css" href="http://www.naturalearthdata.com/wp-content/themes/NEV/ie.css" />
-<![endif]-->
-<script src="http://www.naturalearthdata.com/wp-content/themes/NEV/js/jquery-1.2.6.min.js" type="text/javascript" charset="utf-8"></script>
-<script>
-     jQuery.noConflict();
-</script>
-<script type="text/javascript" charset="utf-8">
-	$(function(){
-		var tabContainers = $('div#maintabdiv > div');
-		tabContainers.hide().filter('#comments').show();
-		
-		$('div#maintabdiv ul#tabnav a').click(function () {
-				tabContainers.hide();
-				tabContainers.filter(this.hash).show();
-				$('div#maintabdiv ul#tabnav a').removeClass('current');
-				$(this).addClass('current');
-				return false;
-			}).filter('#comments').click();
-		
-		
-	});
-</script>
-
-		<script type="text/javascript" language="javascript" src="http://www.naturalearthdata.com/dataTables/media/js/jquery.dataTables.js"></script>
-		<script type="text/javascript" charset="utf-8">
-			$(document).ready(function() {
-				$('#ne_table').dataTable();
-			} );
-		</script>
-
-</head>
-<body>
-<div id="page">
-<div id="header">
-	<div id="headerimg">		
-        <h1><a href="http://www.naturalearthdata.com/"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/nev_logo.png" alt="Natural Earth title="Natural Earth" /></a></h1> 
-        <div class="description">Free vector and raster map data at 1:10m, 1:50m, and 1:110m scales</div> 
-        <div class="header_search"><form method="get" id="searchform" action="http://www.naturalearthdata.com/">
-<label class="hidden" for="s">Search for:</label>
-<div><input type="text" value="" name="s" id="s" />
-<input type="submit" id="searchsubmit" value="Search" />
-</div>
-</form>
-</div>
-<!--<div class="translate_panel" style="align:top; margin-left:650px; top:50px;">
-<div id="google_translate_element" style="float:left;"></div>
-<script>
-function googleTranslateElementInit() {
- new google.translate.TranslateElement({
-   pageLanguage: 'en'
- }, 'google_translate_element');
-}
-</script>
-<script src="http://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"></script>
-</div>-->
-	</div>
-    
-</div>
-
-<div id="pagemenu" style="align:bottom;">
-    <ul id="page-list" class="clearfix"><li class="page_item page-item-4"><a href="http://www.naturalearthdata.com" title="Home">Home</a></li>
-<li class="page_item page-item-10"><a href="http://www.naturalearthdata.com/features/" title="Features">Features</a></li>
-<li class="page_item page-item-12"><a href="http://www.naturalearthdata.com/downloads/" title="Downloads">Downloads</a></li>
-<li class="page_item page-item-6 current_page_parent"><a href="http://www.naturalearthdata.com/blog/" title="Blog">Blog</a></li>
-<li class="page_item page-item-14"><a href="http://www.naturalearthdata.com/forums" title="Forums">Forums</a></li>
-<li class="page_item page-item-366"><a href="http://www.naturalearthdata.com/corrections" title="Corrections">Corrections</a></li>
-<li class="page_item page-item-16"><a href="http://www.naturalearthdata.com/about/" title="About">About</a></li>
-</ul>    
-</div>
-
-<hr />	<div id="main">
-	<div id="content" class="narrowcolumn">
-
-				
-									&laquo; <a href="http://www.naturalearthdata.com/downloads/10m-cultural-vectors/">1:10m Cultural Vectors</a>&nbsp;
-						   <div class="post" id="post-472">
-       		<h2>Populated Places</h2>
-			<div class="entry">
-				<div class="downloadPromoBlock" style="float: left;">
-<div style="float: left; width: 170px;"><img class="alignnone size-full wp-image-1918" title="pop_thumb" src="http://www.naturalearthdata.com/wp-content/uploads/2009/09/pop_thumb.png" alt="pop_thumb" width="150" height="97" /></div>
-<div style="float: left; width: 410px;"><em>City and town points, from Tokyo to Wasilla, Cairo to Kandahar</em>
-<div class="download-link-div">
-	<a class="download-link" rel="nofollow" title="Downloaded 26754 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_populated_places.zip');">Download populated places</a> <span class="download-link-span">(1.51 MB) version 2.0.0</span> 
-</div> <div class="download-link-div">
-	<a class="download-link" rel="nofollow" title="Downloaded 2515 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places_simple.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_populated_places_simple.zip');">Download simple (less columns)</a> <span class="download-link-span">(719.87 KB) version 2.0.0</span> 
-</div>
-<span id="more-472"></span></div>
-</div>
-<div class="downloadMainBlock" style="float: left;">
-<p><strong>About</strong></p>
-<p>Point symbols with name attributes. Includes all admin-0 and many admin-1 capitals, major cities and towns, plus a sampling of smaller towns in sparsely inhabited regions. We favor regional significance over population census in determining our selection of places. Use the scale rankings to filter the number of towns that appear on your map.</p>
-<p><img class="alignnone size-full wp-image-1920" title="pop_banner" src="http://www.naturalearthdata.com/wp-content/uploads/2009/09/pop_banner.png" alt="pop_banner" width="580" height="150" /></p>
-<p><a href="http://www.ornl.gov/sci/landscan/" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.ornl.gov/sci/landscan/');">LandScan</a> derived population estimates are provided for 90% of our cities. Those lacking population estimates are often in sparsely inhabited areas. We provide a range of population values that account for the total &#8220;metropolitan&#8221; population rather than it&#8217;s administrative boundary population. Use the PopMax column to size your town labels. Starting in version 1.1, popMax has been throttled down to the UN estimated metro population for the ~500 largest urban areas in the world. This affects towns in China, India, and parts of Africa where our Landscan counting method usually over estimated.</p>
-<p>Population estimates were derived from the LANDSCAN dataset maintained and distributed by the Oak Ridge National Laboratory. These data were converted from raster to vector and pixels with fewer than 200 persons per square kilometer were removed from the dataset as they were classified as rural. Once urban pixels were selected, these pixels were aggregated into contiguous units. Concurrently Thiessen polygons were created based on the selected city points. The Thiessen polygons were used to intersect the contiguous city boundaries to produce bounded areas for the cities. As a result, our estimates capture a metropolitan and micropolitan populations per city regardless of administrative units.</p>
-<p>Once intersected, the contiguous polygons were recalculated, using aerial interpolation assuming uniform population distribution within each pixel, to determine the population total. This process was conducted multiple times, for each scale level, to produce population estimates for each city at nested scales of 1:300 million, 1:110 million, 1:50 million, 1:20 million, and 1:10 million. </p>
-<div class="download-link-div">
-	<a class="download-link" rel="nofollow" title="Downloaded 481 times (Shapefile, geoDB, or TIFF format)" onclick="if (window.urchinTracker) urchinTracker ('http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_urban_areas_landscan.zip');" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_urban_areas_landscan.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_urban_areas_landscan.zip');">Download landscan urban areas</a> <span class="download-link-span">(13.01 MB) version 2.0.0</span> 
-</div>
-<p><strong>Population ranks</strong></p>
-<p>Are calculated as rank_max and rank_min using this general VB formula that can be pasted into ArcMap Field Calculator advanced area (set your output to x):</p>
-<blockquote><p>
-a = [pop_max]</p>
-<p>if( a > 10000000 ) then
-x = 14
-elseif( a > 5000000 ) then
-x = 13
-elseif( a > 1000000 ) then
-x = 12
-elseif( a > 500000 ) then
-x = 11
-elseif( a > 200000 ) then
-x = 10
-elseif( a > 100000 ) then
-x = 9
-elseif( a > 50000 ) then
-x = 8
-elseif( a > 20000 ) then
-x = 7
-elseif( a > 10000 ) then
-x = 6
-elseif( a > 5000 ) then
-x = 5
-elseif( a > 2000 ) then
-x = 4
-elseif( a > 1000 ) then
-x = 3
-elseif( a > 200 ) then
-x = 2
-elseif( a > 0 ) then
-x = 1
-else
-x = 0
-end if</p></blockquote>
-<p><strong>Issues</strong></p>
-<p>While we don&#8217;t want to show every admin-1 capital, for those countries where we show most admin-1 capitals, we should have a complete set. If you find we are missing one, please log it in the Cx tool at right.</p>
-<p><strong>Version History</strong></p>
-	<ul>
-					<li>
-									<a rel="nofollow" title="Download version 2.0.0 of ne_10m_populated_places.zip" href="http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_populated_places.zip" onclick="javascript:pageTracker._trackPageview('/downloads/http///download/10m/cultural/ne_10m_populated_places.zip');">2.0.0</a>								
-							</li>
-					<li>
-									1.4.0								
-							</li>
-					<li>
-									1.3.0								
-							</li>
-					<li>
-									1.1.0								
-							</li>
-					<li>
-									0.9.0								
-							</li>
-			</ul>
-
-<p><a href="https://github.com/nvkelso/natural-earth-vector/blob/master/CHANGELOG" onclick="javascript:pageTracker._trackPageview('/outbound/article/https://github.com/nvkelso/natural-earth-vector/blob/master/CHANGELOG');">The master changelog is available on Github »</a>
-</div>
-
-<div class="sociable">
-<div class="sociable_tagline">
-<strong>Share and Enjoy:</strong>
-</div>
-<ul>
-	<li class="sociablefirst"><a rel="nofollow"  target="_blank" href="http://twitter.com/home?status=Populated%20Places%20-%20http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://twitter.com/home?status=Populated%20Places%20-%20http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F');" title="Twitter"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Twitter" alt="Twitter" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-343px -55px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://www.facebook.com/share.php?u=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;t=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.facebook.com/share.php?u=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;t=Populated%20Places');" title="Facebook"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Facebook" alt="Facebook" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-343px -1px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://digg.com/submit?phase=2&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;bodytext=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://digg.com/submit?phase=2&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;bodytext=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie');" title="Digg"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Digg" alt="Digg" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-235px -1px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://delicious.com/post?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;notes=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://delicious.com/post?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;notes=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie');" title="del.icio.us"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="del.icio.us" alt="del.icio.us" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-199px -1px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://www.google.com/bookmarks/mark?op=edit&amp;bkmk=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;annotation=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.google.com/bookmarks/mark?op=edit&amp;bkmk=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;annotation=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie');" title="Google Bookmarks"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Google Bookmarks" alt="Google Bookmarks" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-91px -19px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://slashdot.org/bookmark.pl?title=Populated%20Places&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://slashdot.org/bookmark.pl?title=Populated%20Places&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F');" title="Slashdot"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Slashdot" alt="Slashdot" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-145px -55px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://www.stumbleupon.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.stumbleupon.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places');" title="StumbleUpon"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="StumbleUpon" alt="StumbleUpon" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-217px -55px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="mailto:?subject=Populated%20Places&amp;body=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F" title="email"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif"  title="email" alt="email" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-325px -1px" class="sociable-hovers" /></a></li>
-	<li><a rel="nofollow"  target="_blank" href="http://www.linkedin.com/shareArticle?mini=true&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;source=Natural+Earth+Free+vector+and+raster+map+data+at+1%3A10m%2C+1%3A50m%2C+and+1%3A110m+scales&amp;summary=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://www.linkedin.com/shareArticle?mini=true&amp;url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places&amp;source=Natural+Earth+Free+vector+and+raster+map+data+at+1%3A10m%2C+1%3A50m%2C+and+1%3A110m+scales&amp;summary=%0D%0A%0D%0ACity%20and%20town%20points%2C%20from%20Tokyo%20to%20Wasilla%2C%20Cairo%20to%20Kandahar%0D%0A%5Bdrain%20file%2039%20show%20nev_download%5D%20%5Bdrain%20file%20224%20show%20nev_download%5D%0D%0A%0D%0A%0D%0A%0D%0A%0D%0AAbout%0D%0A%0D%0APoint%20symbols%20with%20name%20attributes.%20Includes%20all%20admin-0%20and%20many%20admin-1%20capitals%2C%20major%20citie');" title="LinkedIn"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="LinkedIn" alt="LinkedIn" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-1px -37px" class="sociable-hovers" /></a></li>
-	<li class="sociablelast"><a rel="nofollow"  target="_blank" href="http://reddit.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places" onclick="javascript:pageTracker._trackPageview('/outbound/article/http://reddit.com/submit?url=http%3A%2F%2Fwww.naturalearthdata.com%2Fdownloads%2F10m-cultural-vectors%2F10m-populated-places%2F&amp;title=Populated%20Places');" title="Reddit"><img src="http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.gif" title="Reddit" alt="Reddit" style="width: 16px; height: 16px; background: transparent url(http://www.naturalearthdata.com/wp-content/plugins/sociable/images/services-sprite.png) no-repeat; background-position:-55px -55px" class="sociable-hovers" /></a></li>
-</ul>
-</div>
-
-				
-			</div>
-
-		</div>
-		
-
-		</div>
-
-
-	<div id="sidebar">
-    	<ul><li id='text-5' class='widget widget_text'><h2 class="widgettitle">Stay up to Date</h2>
-			<div class="textwidget"> Know when a new version of Natural Earth is released by subscribing to our <a href="http://www.naturalearthdata.com/updates/" class="up-to-date-link" >announcement list</a>.</div>
-		</li></ul><ul><li id='text-2' class='widget widget_text'><h2 class="widgettitle">Find a Problem?</h2>
-			<div class="textwidget"><div>
-<div style="float:left; width:65px;"><a href="/corrections/index.php?a=add"><img class="alignleft" title="New Ticket" src="http://www.naturalearthdata.com/corrections/img/newticket.png" alt="" width="60" height="60" /></a></div><div class="textwidget" style="float:left;width:120px; font-size:1.2em; font-size-adjust:none; font-style:normal;
-font-variant:normal; font-weight:normal; line-height:normal;">Submit suggestions and bug reports via our <a href="/corrections/index.php?a=add">correction system</a> and track the progress of your edits.</div>
-</div></div>
-		</li></ul><ul><li id='text-3' class='widget widget_text'><h2 class="widgettitle">Join Our Community</h2>
-			<div class="textwidget"><div>
-<div style="float:left; width:65px;"><a href="/forums/"><img src="http://www.naturalearthdata.com/wp-content/uploads/2009/08/green_globe_chat_bubble_562e.png" alt="forums" title="Chat in the forum!" width="50" height="50" /></a></div><div class="textwidget" style="float:left;width:120px; font-size:1.2em; font-size-adjust:none; font-style:normal;
-font-variant:normal; font-weight:normal; line-height:normal;">Talk back and discuss Natural Earth in the <a href="/forums/">Forums</a>.</div>
-</div></div>
-		</li></ul><ul><li id='text-4' class='widget widget_text'><h2 class="widgettitle">Thank You</h2>
-			<div class="textwidget">Our data downloads are generously hosted by Florida State University.</div>
-		</li></ul>	</div>
-
-</div>
-
-<hr />
-<div id="footer">
-<div id="footerarea">
-	<div id="footerlogos">
-    	<p>Supported by:</p>
-        <div class="footer-ad-box">
-        	<a href="http://www.nacis.org" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/nacis.png" alt="NACIS" /></a>
-        </div>
-    	<div class="footer-ad-box">
-        	<a href="http://www.cartotalk.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/cartotalk_ad.png" alt="Cartotalk" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.mapgiving.org" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/mapgiving.png" alt="Mapgiving" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.geography.wisc.edu/cartography/" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/wisconsin.png" alt="University of Wisconsin Madison - Cartography Dept." /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.shadedrelief.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/shaded_relief.png" alt="Shaded Relief" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.xnrproductions.com " target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/xnr.png" alt="XNR Productions" /></a>
-        </div>
-        
-        <p style="clear:both;"></p>
-        
-       <div class="footer-ad-box">
-        	<a href="http://www.freac.fsu.edu" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/fsu.png" alt="Florida State University - FREAC" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.springercartographics.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/scllc.png" alt="Springer Cartographics LLC" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.washingtonpost.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/wpost.png" alt="Washington Post" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.redgeographics.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/redgeo.png" alt="Red Geographics" /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://kelsocartography.com/blog " target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/kelso.png" alt="Kelso Cartography" /></a>
-        </div>
-        
-        <p style="clear:both;"></p>
-        <div class="footer-ad-box">
-        	<a href="http://www.avenza.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/avenza.png" alt="Avenza Systems Inc." /></a>
-        </div>
-        <div class="footer-ad-box">
-        	<a href="http://www.stamen.com" target="_blank"><img src="http://www.naturalearthdata.com/wp-content/themes/NEV/images/stamen_ne_logo.png" alt="Stamen Design" /></a>
-        </div>
-
-
-    </div>
-    <p style="clear:both;"></p>
-	<span id="footerleft">
-		&copy; 2012. Natural Earth. All rights reserved.
-	</span>
-    <span id="footerright"> 
-    	<!-- Please help promote WordPress and simpleX. Do not remove -->   
-		<div>Powered by <a href="http://wordpress.org/">WordPress</a></div>
-        <div><a href="http://www.naturalearthdata.com/wp-admin">Staff Login &raquo;</a></div>
-    </span>
-</div>
-</div>
-		
-<!-- Google Analytics for WordPress | http://yoast.com/wordpress/google-analytics/ -->
-<script type="text/javascript">
-	var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
-	document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
-</script>
-<script type="text/javascript">
-	try {
-		var pageTracker = _gat._getTracker("UA-10168306-1");
-	} catch(err) {}
-</script>
-<script src="http://www.naturalearthdata.com/wp-content/plugins/google-analytics-for-wordpress/custom_se.js" type="text/javascript"></script>
-<script type="text/javascript">
-	try {
-		// Cookied already: 
-		pageTracker._trackPageview();
-	} catch(err) {}
-</script>
-<!-- End of Google Analytics code -->
-
-</body>
-</html>
\ No newline at end of file
diff --git a/geopandas/datasets/naturalearth_creation.py b/geopandas/datasets/naturalearth_creation.py
deleted file mode 100644
index 2942deb2..00000000
--- a/geopandas/datasets/naturalearth_creation.py
+++ /dev/null
@@ -1,128 +0,0 @@
-"""
-Script that generates the included dataset 'naturalearth_lowres.shp'
-and 'naturalearth_cities.shp'.
-
-Raw data: https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/110m/cultural/ne_110m_admin_0_countries.zip
-Current version used: see code
-"""
-
-import geopandas as gpd
-import requests
-from pathlib import Path
-from zipfile import ZipFile
-import tempfile
-from shapely.geometry import box
-
-version = "latest"
-urlbase = "https://www.naturalearthdata.com/"
-urlbase += "http//www.naturalearthdata.com/download/110m/cultural/"
-
-
-def countries_override(world_raw):
-    # not ideal - fix some country codes
-    mask = world_raw["ISO_A3"].eq("-99") & world_raw["TYPE"].isin(
-        ["Sovereign country", "Country"]
-    )
-    world_raw.loc[mask, "ISO_A3"] = world_raw.loc[mask, "ADM0_A3"]
-    # backwards compatibility
-    return world_raw.rename(columns={"GDP_MD": "GDP_MD_EST"})
-
-
-# any change between versions?
-def df_same(new, old, dataset, log):
-    assert (new.columns == old.columns).all(), "columns should be the same"
-    if new.shape != old.shape:
-        dfc = old.merge(new, on="name", how="outer", suffixes=("_old", "_new")).loc[
-            lambda d: d.isna().any(axis=1)
-        ]
-        log.append(f"### {dataset} row count changed ###\n{dfc.to_markdown()}")
-        return False
-    dfc = new.compare(old)
-    if len(dfc) > 0:
-        log.append(f"### {dataset} data changed ###\n{dfc.to_markdown()}")
-    return len(dfc) == 0
-
-
-config = [
-    {
-        "file": "ne_110m_populated_places.zip",
-        "cols": ["NAME", "geometry"],
-        "current": gpd.datasets.get_path("naturalearth_cities"),
-    },
-    {
-        "file": "ne_110m_admin_0_countries.zip",
-        "cols": ["POP_EST", "CONTINENT", "NAME", "ISO_A3", "GDP_MD_EST", "geometry"],
-        "override": countries_override,
-        "current": gpd.datasets.get_path("naturalearth_lowres"),
-    },
-]
-
-downloads = {}
-log = []
-for dl in config:
-    with tempfile.TemporaryDirectory() as tmpdirname:
-        url = urlbase + dl["file"]
-        r = requests.get(
-            url,
-            stream=True,
-            headers={"User-Agent": "XY"},
-            params=None if version == "latest" else {"version": version},
-        )
-        assert (
-            r.status_code == 200
-        ), f"version: {version} does not exist. status: {r.status_code}"
-
-        f = Path(tmpdirname).joinpath(dl["file"])
-        with open(f, "wb") as fd:
-            for chunk in r.iter_content(chunk_size=128):
-                fd.write(chunk)
-        # extract the natural earth version
-        z = ZipFile(f)
-        version_f = [i for i in z.infolist() if "VERSION" in i.filename]
-        assert len(version_f) == 1, "failed to find VERSION file"
-        with open(z.extract(version_f[0], Path(tmpdirname).joinpath("v.txt"))) as f_:
-            dl_version = f_.read().strip()
-
-        # extract geodataframe from zip
-        gdf = gpd.read_file(f)
-        # maintain structure that geopandas distributes
-        if "override" in dl.keys():
-            gdf = dl["override"](gdf)
-        gdf = gdf.loc[:, dl["cols"]]
-        gdf = gdf.rename(columns={c: c.lower() for c in gdf.columns})
-
-        # override Crimea #2382
-        if dl["file"] == "ne_110m_admin_0_countries.zip":
-            crimean_bbox = box(32.274, 44.139, 36.65, 46.704)
-            crimea_only = (
-                gdf.loc[gdf.name == "Russia", "geometry"]
-                .iloc[0]
-                .intersection(crimean_bbox)
-            )
-            complete_ukraine = (
-                gdf.loc[gdf.name == "Ukraine", "geometry"].iloc[0].union(crimea_only)
-            )
-            correct_russia = (
-                gdf.loc[gdf.name == "Russia", "geometry"]
-                .iloc[0]
-                .difference(crimean_bbox)
-            )
-            r_ix = gdf.loc[gdf.name == "Russia"].index[0]
-            gdf.at[r_ix, "geometry"] = correct_russia
-
-            u_ix = gdf.loc[gdf.name == "Ukraine"].index[0]
-            gdf.at[u_ix, "geometry"] = complete_ukraine
-
-        # get changes between current version and new version
-        if not df_same(gdf, gpd.read_file(dl["current"]), dl["file"], log):
-            downloads[dl["file"]] = gdf
-
-
-# create change log that can be pasted into PR
-with open(f"CHANGE_{dl_version}.md", "w") as f:
-    f.write("\n\n".join(log))
-
-# save downloaded geodataframe to appropriate place
-for k, gdf_ in downloads.items():
-    f = [Path(c["current"]) for c in config if c["file"] == k][0]
-    gdf_.to_file(driver="ESRI Shapefile", filename=Path(f.parent.name).joinpath(f.name))
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 930be848..751d4814 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -395,6 +395,26 @@ class GeoDataFrame(GeoPandasBase, DataFrame):
             self.rename(columns={geometry_col: col}, inplace=inplace)
             self.set_geometry(col, inplace=inplace)
 
+    @property
+    def active_geometry_name(self):
+        """Return the name of the active geometry column
+
+        Returns a string name if a GeoDataFrame has an active geometry column set.
+        Otherwise returns None. You can also access the active geometry column using the
+        ``.geometry`` property. You can set a GeoSeries to be an active geometry
+        using the :meth:`~GeoDataFrame.set_geometry` method.
+
+        Returns
+        -------
+        str
+            name of an active geometry column or None
+
+        See also
+        --------
+        GeoDataFrame.set_geometry : set the active geometry
+        """
+        return self._geometry_column_name
+
     @property
     def crs(self):
         """
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index d9a6636c..98fe7f8a 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -18,6 +18,7 @@ from urllib.parse import urlparse as parse_url
 from urllib.parse import uses_netloc, uses_params, uses_relative
 import urllib.request
 
+from geopandas._compat import PANDAS_GE_20
 
 _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
 _VALID_URLS.discard("")
@@ -408,7 +409,10 @@ def _read_file_fiona(
                 # fiona only supports up to ms precision (any microseconds are
                 # floating point rounding error)
                 if not (as_dt.dtype == "object"):
-                    df[k] = as_dt.dt.round(freq="ms")
+                    if PANDAS_GE_20:
+                        df[k] = as_dt.dt.as_unit("ms")
+                    else:
+                        df[k] = as_dt.dt.round(freq="ms")
             return df
 
 
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index ad17f42f..19b8a97d 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -484,7 +484,7 @@ def plot_series(
             ax, points, values_, color=color_, cmap=cmap, **style_kwds
         )
 
-    plt.draw()
+    ax.figure.canvas.draw_idle()
     return ax
 
 
@@ -957,7 +957,7 @@ def plot_dataframe(
             n_cmap.set_array(np.array([]))
             ax.get_figure().colorbar(n_cmap, **legend_kwds)
 
-    plt.draw()
+    ax.figure.canvas.draw_idle()
     return ax
 
 
diff --git a/geopandas/sindex.py b/geopandas/sindex.py
index da5f2443..34ba0650 100644
--- a/geopandas/sindex.py
+++ b/geopandas/sindex.py
@@ -5,6 +5,12 @@ import shapely
 from shapely.geometry.base import BaseGeometry
 
 from . import array, geoseries
+from . import _compat as compat
+
+PREDICATES = {p.name for p in shapely.strtree.BinaryPredicate} | {None}
+
+if compat.GEOS_GE_310:
+    PREDICATES.update(["dwithin"])
 
 
 class SpatialIndex:
@@ -44,11 +50,11 @@ class SpatialIndex:
         >>> s = geopandas.GeoSeries([Point(0, 0), Point(1, 1)])
         >>> s.sindex.valid_query_predicates  # doctest: +SKIP
         {None, "contains", "contains_properly", "covered_by", "covers", \
-"crosses", "intersects", "overlaps", "touches", "within"}
+"crosses", "dwithin", "intersects", "overlaps", "touches", "within"}
         """
-        return {p.name for p in shapely.strtree.BinaryPredicate} | {None}
+        return PREDICATES
 
-    def query(self, geometry, predicate=None, sort=False):
+    def query(self, geometry, predicate=None, sort=False, distance=None):
         """
         Return the integer indices of all combinations of each input geometry
         and tree geometries where the bounding box of each input geometry
@@ -67,6 +73,8 @@ class SpatialIndex:
         to those that meet the predicate when comparing the input geometry to
         the tree geometry: ``predicate(geometry, tree_geometry)``.
 
+        The 'dwithin' predicate requires GEOS >= 3.10.
+
         Bounding boxes are limited to two dimensions and are axis-aligned
         (equivalent to the ``bounds`` property of a geometry); any Z values
         present in input geometries are ignored when querying the tree.
@@ -83,7 +91,7 @@ class SpatialIndex:
             iterables (GeoSeries, GeometryArray) or a numpy array of Shapely
             geometries.
         predicate : {None, "contains", "contains_properly", "covered_by", "covers", \
-"crosses", "intersects", "overlaps", "touches", "within"}, optional
+"crosses", "intersects", "overlaps", "touches", "within", "dwithin"}, optional
             If predicate is provided, the input geometries are tested
             using the predicate function against each item in the tree
             whose extent intersects the envelope of the input geometry:
@@ -97,6 +105,10 @@ class SpatialIndex:
             as the secondary key.
             If False, no additional sorting is applied (results are often
             sorted but there is no guarantee).
+        distance : number or array_like, optional
+            Distances around each input geometry within which to query the tree for
+            the 'dwithin' predicate. If array_like, shape must be broadcastable to shape
+            of geometry. Required if ``predicate='dwithin'``.
 
         Returns
         -------
@@ -151,6 +163,12 @@ class SpatialIndex:
         array([[0],
                [3]])
 
+        >>> s.sindex.query(box(1, 1, 3, 3), predicate="dwithin", distance=0)
+        array([1, 2, 3])
+
+        >>> s.sindex.query(box(1, 1, 3, 3), predicate="dwithin", distance=2)
+        array([0, 1, 2, 3, 4])
+
         Notes
         -----
         In the context of a spatial join, input geometries are the "left"
@@ -161,14 +179,36 @@ class SpatialIndex:
         optional predicate are returned.
         """
         if predicate not in self.valid_query_predicates:
+            if predicate == "dwithin":
+                raise ValueError("predicate = 'dwithin' requires GEOS >= 3.10.0")
+
             raise ValueError(
-                "Got `predicate` = `{}`; ".format(predicate)
+                "Got predicate='{}'; ".format(predicate)
                 + "`predicate` must be one of {}".format(self.valid_query_predicates)
             )
 
+        # distance argument requirement of predicate `dwithin`
+        # and only valid for predicate `dwithin`
+        kwargs = {}
+        if predicate == "dwithin":
+            if distance is None:
+                # the distance parameter is needed
+                raise ValueError(
+                    "'distance' parameter is required for 'dwithin' predicate"
+                )
+            # add distance to kwargs
+            kwargs["distance"] = distance
+
+        elif distance is not None:
+            # distance parameter is invalid
+            raise ValueError(
+                "'distance' parameter is only supported in combination with "
+                "'dwithin' predicate"
+            )
+
         geometry = self._as_geometry_array(geometry)
 
-        indices = self._tree.query(geometry, predicate=predicate)
+        indices = self._tree.query(geometry, predicate=predicate, **kwargs)
 
         if sort:
             if indices.ndim == 1:
@@ -209,7 +249,7 @@ class SpatialIndex:
         else:
             return np.asarray(geometry)
 
-    def query_bulk(self, geometry, predicate=None, sort=False):
+    def query_bulk(self, geometry, predicate=None, sort=False, distance=None):
         """
         DEPRECATED: use `query` instead.
 
@@ -283,7 +323,7 @@ class SpatialIndex:
             FutureWarning,
             stacklevel=2,
         )
-        return self.query(geometry, predicate=predicate, sort=sort)
+        return self.query(geometry, predicate=predicate, sort=sort, distance=distance)
 
     def nearest(
         self,
diff --git a/geopandas/datasets/README.md b/geopandas/tests/data/README.md
similarity index 97%
rename from geopandas/datasets/README.md
rename to geopandas/tests/data/README.md
index 625dbfca..c2e736de 100644
--- a/geopandas/datasets/README.md
+++ b/geopandas/tests/data/README.md
@@ -1,4 +1,4 @@
-# Datasets included with geopandas
+# Datasets previously included with geopandas
 
 - `'nybb'`: Borough boundaries of New York City, data provided Department of City Planning (DCP), https://data.cityofnewyork.us/City-Government/Borough-Boundaries/tqmj-j8zm
 - `'naturalearth_cities'`: capital cities, based on http://www.naturalearthdata.com/downloads/10m-cultural-vectors/110m-populated-places/
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
index ff27211b..c7477edc 100644
--- a/geopandas/tools/overlay.py
+++ b/geopandas/tools/overlay.py
@@ -65,8 +65,8 @@ def _overlay_intersection(df1, df2):
             right_index=True,
             suffixes=("_1", "_2"),
         )
-        result["__idx1"] = None
-        result["__idx2"] = None
+        result["__idx1"] = np.nan
+        result["__idx2"] = np.nan
         return result[
             result.columns.drop(df1.geometry.name).tolist() + [df1.geometry.name]
         ]
@@ -135,14 +135,7 @@ def _overlay_union(df1, df2):
     """
     dfinter = _overlay_intersection(df1, df2)
     dfsym = _overlay_symmetric_diff(df1, df2)
-    with warnings.catch_warnings():
-        # pandas GH52532 FutureWarning, fix new behaviour if needed when it is added
-        warnings.filterwarnings(
-            "ignore",
-            "The behavior of DataFrame concatenation with empty",
-            FutureWarning,
-        )
-        dfunion = pd.concat([dfinter, dfsym], ignore_index=True, sort=False)
+    dfunion = pd.concat([dfinter, dfsym], ignore_index=True, sort=False)
     # keep geometry column last
     columns = list(dfunion.columns)
     columns.remove("geometry")
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
index 5da51223..f717f47f 100644
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -15,6 +15,7 @@ def sjoin(
     predicate="intersects",
     lsuffix="left",
     rsuffix="right",
+    distance=None,
     **kwargs,
 ):
     """Spatial join of two GeoDataFrames.
@@ -42,6 +43,11 @@ def sjoin(
         Suffix to apply to overlapping column names (left GeoDataFrame).
     rsuffix : string, default 'right'
         Suffix to apply to overlapping column names (right GeoDataFrame).
+    distance : number or array_like, optional
+        Distance(s) around each input geometry within which to query the tree
+        for the 'dwithin' predicate. If array_like, must be
+        one-dimesional with length equal to length of left GeoDataFrame.
+        Required if ``predicate='dwithin'``.
 
     Examples
     --------
@@ -117,7 +123,7 @@ def sjoin(
 
     _basic_checks(left_df, right_df, how, lsuffix, rsuffix)
 
-    indices = _geom_predicate_query(left_df, right_df, predicate)
+    indices = _geom_predicate_query(left_df, right_df, predicate, distance)
 
     joined = _frame_join(indices, left_df, right_df, how, lsuffix, rsuffix)
 
@@ -174,7 +180,7 @@ def _basic_checks(left_df, right_df, how, lsuffix, rsuffix):
         )
 
 
-def _geom_predicate_query(left_df, right_df, predicate):
+def _geom_predicate_query(left_df, right_df, predicate, distance):
     """Compute geometric comparisons and get matching indices.
 
     Parameters
@@ -213,7 +219,9 @@ def _geom_predicate_query(left_df, right_df, predicate):
             input_geoms = left_df.geometry
 
     if sindex:
-        l_idx, r_idx = sindex.query(input_geoms, predicate=predicate, sort=False)
+        l_idx, r_idx = sindex.query(
+            input_geoms, predicate=predicate, sort=False, distance=distance
+        )
         indices = pd.DataFrame({"_key_left": l_idx, "_key_right": r_idx})
     else:
         # when sindex is empty / has no valid geometries
diff --git a/pyproject.toml b/pyproject.toml
index bdac183e..00af3489 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,10 +54,7 @@ include = ["geopandas", "geopandas.*"]
 
 [tool.setuptools.package-data]
 geopandas = [
-    "datasets/nybb_16a.zip",
-    "datasets/naturalearth_cities/*",
-    "datasets/naturalearth_lowres/*",
-    "tests/data/*",
+    "tests/data/null_geom.geojson"
 ]
 
 [tool.pytest.ini_options]
@@ -148,6 +145,8 @@ ignore = [ # space before : (needed for how black formats slicing)
     # "B301",  # not yet implemented
     # Only works with python >=3.10
     "B905",
+    # dict literals
+    "C408",
     # Too many arguments to function call
     "PLR0913",
     # Too many returns
@@ -195,7 +194,7 @@ ignore = [ # space before : (needed for how black formats slicing)
     # explicit-f-string-type-conversion
     "RUF010",
 ]
-exclude = ["doc/*", "benchmarks/*", "versioneer.py", "geopandas/_version.py"]
+extend-exclude = ["doc/*", "benchmarks/*", "versioneer.py", "geopandas/_version.py"]
 
 [tool.ruff.per-file-ignores]
 "geopandas/__init__.py" = ["F401"]


Test patch:
diff --git a/geopandas/conftest.py b/geopandas/conftest.py
index 601c92fc..72ce280c 100644
--- a/geopandas/conftest.py
+++ b/geopandas/conftest.py
@@ -1,7 +1,46 @@
+import os.path
+
 import pytest
 import geopandas
+from geopandas.tests.util import _NYBB, _NATURALEARTH_LOWRES, _NATURALEARTH_CITIES
 
 
 @pytest.fixture(autouse=True)
 def add_geopandas(doctest_namespace):
     doctest_namespace["geopandas"] = geopandas
+
+
+# Datasets used in our tests
+
+
+@pytest.fixture(scope="session")
+def naturalearth_lowres() -> str:
+    # skip if data missing, unless on github actions
+    if os.path.isfile(_NATURALEARTH_LOWRES) or os.getenv("GITHUB_ACTIONS"):
+        return _NATURALEARTH_LOWRES
+    else:
+        pytest.skip("Naturalearth lowres dataset not found")
+
+
+@pytest.fixture(scope="session")
+def naturalearth_cities() -> str:
+    # skip if data missing, unless on github actions
+    if os.path.isfile(_NATURALEARTH_CITIES) or os.getenv("GITHUB_ACTIONS"):
+        return _NATURALEARTH_CITIES
+    else:
+        pytest.skip("Naturalearth cities dataset not found")
+
+
+@pytest.fixture(scope="session")
+def nybb_filename() -> str:
+    # skip if data missing, unless on github actions
+    if os.path.isfile(_NYBB[len("zip://") :]) or os.getenv("GITHUB_ACTIONS"):
+        return _NYBB
+    else:
+        pytest.skip("NYBB dataset not found")
+
+
+@pytest.fixture(scope="class")
+def _setup_class_nybb_filename(nybb_filename, request):
+    """Attach nybb_filename class attribute for unittest style setup_method"""
+    request.cls.nybb_filename = nybb_filename
diff --git a/geopandas/io/tests/test_arrow.py b/geopandas/io/tests/test_arrow.py
index 28b4190c..75ab9ec4 100644
--- a/geopandas/io/tests/test_arrow.py
+++ b/geopandas/io/tests/test_arrow.py
@@ -17,7 +17,6 @@ from shapely.geometry import box, Point, MultiPolygon
 import geopandas
 from geopandas import GeoDataFrame, read_file, read_parquet, read_feather
 from geopandas.array import to_wkb
-from geopandas.datasets import get_path
 from geopandas.io.arrow import (
     SUPPORTED_VERSIONS,
     _create_metadata,
@@ -60,9 +59,8 @@ def file_format(request):
         return read_feather, GeoDataFrame.to_feather
 
 
-def test_create_metadata():
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+def test_create_metadata(naturalearth_lowres):
+    df = read_file(naturalearth_lowres)
     metadata = _create_metadata(df)
 
     assert isinstance(metadata, dict)
@@ -125,9 +123,8 @@ def test_decode_metadata():
     assert _decode_metadata(None) is None
 
 
-def test_validate_dataframe():
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+def test_validate_dataframe(naturalearth_lowres):
+    df = read_file(naturalearth_lowres)
 
     # valid: should not raise ValueError
     _validate_dataframe(df)
@@ -290,11 +287,11 @@ def test_pandas_parquet_roundtrip1(tmpdir):
 
 
 @pytest.mark.parametrize(
-    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb"]
+    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb_filename"]
 )
-def test_pandas_parquet_roundtrip2(test_dataset, tmpdir):
-    test_dataset = "naturalearth_lowres"
-    df = DataFrame(read_file(get_path(test_dataset)).drop(columns=["geometry"]))
+def test_pandas_parquet_roundtrip2(test_dataset, tmpdir, request):
+    path = request.getfixturevalue(test_dataset)
+    df = DataFrame(read_file(path).drop(columns=["geometry"]))
 
     filename = os.path.join(str(tmpdir), "test.pq")
     df.to_parquet(filename)
@@ -305,15 +302,16 @@ def test_pandas_parquet_roundtrip2(test_dataset, tmpdir):
 
 
 @pytest.mark.parametrize(
-    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb"]
+    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb_filename"]
 )
-def test_roundtrip(tmpdir, file_format, test_dataset):
+def test_roundtrip(tmpdir, file_format, test_dataset, request):
     """Writing to parquet should not raise errors, and should not alter original
     GeoDataFrame
     """
+    path = request.getfixturevalue(test_dataset)
     reader, writer = file_format
 
-    df = read_file(get_path(test_dataset))
+    df = read_file(path)
     orig = df.copy()
 
     filename = os.path.join(str(tmpdir), "test.pq")
@@ -332,14 +330,13 @@ def test_roundtrip(tmpdir, file_format, test_dataset):
     assert_geodataframe_equal(df, pq_df)
 
 
-def test_index(tmpdir, file_format):
+def test_index(tmpdir, file_format, naturalearth_lowres):
     """Setting index=`True` should preserve index in output, and
     setting index=`False` should drop index from output.
     """
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset)).set_index("iso_a3")
+    df = read_file(naturalearth_lowres).set_index("iso_a3")
 
     filename = os.path.join(str(tmpdir), "test_with_index.pq")
     writer(df, filename, index=True)
@@ -353,13 +350,12 @@ def test_index(tmpdir, file_format):
 
 
 @pytest.mark.parametrize("compression", ["snappy", "gzip", "brotli", None])
-def test_parquet_compression(compression, tmpdir):
+def test_parquet_compression(compression, tmpdir, naturalearth_lowres):
     """Using compression options should not raise errors, and should
     return identical GeoDataFrame.
     """
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     filename = os.path.join(str(tmpdir), "test.pq")
     df.to_parquet(filename, compression=compression)
@@ -374,13 +370,12 @@ def test_parquet_compression(compression, tmpdir):
     reason="Feather only supported for pyarrow >= 0.17",
 )
 @pytest.mark.parametrize("compression", ["uncompressed", "lz4", "zstd"])
-def test_feather_compression(compression, tmpdir):
+def test_feather_compression(compression, tmpdir, naturalearth_lowres):
     """Using compression options should not raise errors, and should
     return identical GeoDataFrame.
     """
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     filename = os.path.join(str(tmpdir), "test.feather")
     df.to_feather(filename, compression=compression)
@@ -390,14 +385,13 @@ def test_feather_compression(compression, tmpdir):
     assert_geodataframe_equal(df, pq_df)
 
 
-def test_parquet_multiple_geom_cols(tmpdir, file_format):
+def test_parquet_multiple_geom_cols(tmpdir, file_format, naturalearth_lowres):
     """If multiple geometry columns are present when written to parquet,
     they should all be returned as such when read from parquet.
     """
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
     df["geom2"] = df.geometry.copy()
 
     filename = os.path.join(str(tmpdir), "test.pq")
@@ -413,13 +407,12 @@ def test_parquet_multiple_geom_cols(tmpdir, file_format):
     assert_geoseries_equal(df.geom2, pq_df.geom2, check_geom_type=True)
 
 
-def test_parquet_missing_metadata(tmpdir):
+def test_parquet_missing_metadata(tmpdir, naturalearth_lowres):
     """Missing geo metadata, such as from a parquet file created
     from a pandas DataFrame, will raise a ValueError.
     """
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     # convert to DataFrame
     df = DataFrame(df)
@@ -473,7 +466,7 @@ def test_parquet_missing_metadata2(tmpdir):
         ),
     ],
 )
-def test_parquet_invalid_metadata(tmpdir, geo_meta, error):
+def test_parquet_invalid_metadata(tmpdir, geo_meta, error, naturalearth_lowres):
     """Has geo metadata with missing required fields will raise a ValueError.
 
     This requires writing the parquet file directly below, so that we can
@@ -482,8 +475,7 @@ def test_parquet_invalid_metadata(tmpdir, geo_meta, error):
 
     from pyarrow import parquet, Table
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     # convert to DataFrame and encode geometry to WKB
     df = DataFrame(df)
@@ -501,14 +493,13 @@ def test_parquet_invalid_metadata(tmpdir, geo_meta, error):
         read_parquet(filename)
 
 
-def test_subset_columns(tmpdir, file_format):
+def test_subset_columns(tmpdir, file_format, naturalearth_lowres):
     """Reading a subset of columns should correctly decode selected geometry
     columns.
     """
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     filename = os.path.join(str(tmpdir), "test.pq")
     writer(df, filename)
@@ -522,14 +513,13 @@ def test_subset_columns(tmpdir, file_format):
         reader(filename, columns=["name"])
 
 
-def test_promote_secondary_geometry(tmpdir, file_format):
+def test_promote_secondary_geometry(tmpdir, file_format, naturalearth_lowres):
     """Reading a subset of columns that does not include the primary geometry
     column should promote the first geometry column present.
     """
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
     df["geom2"] = df.geometry.copy()
 
     filename = os.path.join(str(tmpdir), "test.pq")
@@ -552,13 +542,12 @@ def test_promote_secondary_geometry(tmpdir, file_format):
     )
 
 
-def test_columns_no_geometry(tmpdir, file_format):
+def test_columns_no_geometry(tmpdir, file_format, naturalearth_lowres):
     """Reading a parquet file that is missing all of the geometry columns
     should raise a ValueError"""
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     filename = os.path.join(str(tmpdir), "test.pq")
     writer(df, filename)
@@ -567,15 +556,13 @@ def test_columns_no_geometry(tmpdir, file_format):
         reader(filename, columns=["name"])
 
 
-def test_missing_crs(tmpdir, file_format):
+def test_missing_crs(tmpdir, file_format, naturalearth_lowres):
     """If CRS is `None`, it should be properly handled
     and remain `None` when read from parquet`.
     """
     reader, writer = file_format
 
-    test_dataset = "naturalearth_lowres"
-
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
     df.crs = None
 
     filename = os.path.join(str(tmpdir), "test.pq")
@@ -600,8 +587,8 @@ def test_default_geo_col_writes(tmp_path):
     Version(pyarrow.__version__) >= Version("0.17.0"),
     reason="Feather only supported for pyarrow >= 0.17",
 )
-def test_feather_arrow_version(tmpdir):
-    df = read_file(get_path("naturalearth_lowres"))
+def test_feather_arrow_version(tmpdir, naturalearth_lowres):
+    df = read_file(naturalearth_lowres)
     filename = os.path.join(str(tmpdir), "test.feather")
 
     with pytest.raises(
@@ -610,7 +597,7 @@ def test_feather_arrow_version(tmpdir):
         df.to_feather(filename)
 
 
-def test_fsspec_url():
+def test_fsspec_url(naturalearth_lowres):
     fsspec = pytest.importorskip("fsspec")
     import fsspec.implementations.memory
 
@@ -624,8 +611,7 @@ def test_fsspec_url():
     fsspec.register_implementation("memory", MyMemoryFileSystem, clobber=True)
     memfs = MyMemoryFileSystem(is_set=True)
 
-    test_dataset = "naturalearth_lowres"
-    df = read_file(get_path(test_dataset))
+    df = read_file(naturalearth_lowres)
 
     with memfs.open("data.parquet", "wb") as f:
         df.to_parquet(f)
@@ -642,10 +628,9 @@ def test_fsspec_url():
     )
 
 
-def test_non_fsspec_url_with_storage_options_raises():
+def test_non_fsspec_url_with_storage_options_raises(naturalearth_lowres):
     with pytest.raises(ValueError, match="storage_options"):
-        test_dataset = "naturalearth_lowres"
-        read_parquet(get_path(test_dataset), storage_options={"foo": "bar"})
+        read_parquet(naturalearth_lowres, storage_options={"foo": "bar"})
 
 
 @pytest.mark.skipif(
@@ -874,10 +859,10 @@ def test_read_gdal_files():
     assert_geodataframe_equal(df, expected, check_crs=True)
 
 
-def test_parquet_read_partitioned_dataset(tmpdir):
+def test_parquet_read_partitioned_dataset(tmpdir, naturalearth_lowres):
     # we don't yet explicitly support this (in writing), but for Parquet it
     # works for reading (by relying on pyarrow.read_table)
-    df = read_file(get_path("naturalearth_lowres"))
+    df = read_file(naturalearth_lowres)
 
     # manually create partitioned dataset
     basedir = tmpdir / "partitioned_dataset"
@@ -889,10 +874,10 @@ def test_parquet_read_partitioned_dataset(tmpdir):
     assert_geodataframe_equal(result, df)
 
 
-def test_parquet_read_partitioned_dataset_fsspec(tmpdir):
+def test_parquet_read_partitioned_dataset_fsspec(tmpdir, naturalearth_lowres):
     fsspec = pytest.importorskip("fsspec")
 
-    df = read_file(get_path("naturalearth_lowres"))
+    df = read_file(naturalearth_lowres)
 
     # manually create partitioned dataset
     memfs = fsspec.filesystem("memory")
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 34180e94..5f983774 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -24,7 +24,7 @@ from geopandas.tests.util import PACKAGE_DIR, validate_boro_df
 try:
     import pyogrio
 
-    PYOGRIO_GE_07 = Version(pyogrio.__version__) > Version("0.6.0")
+    PYOGRIO_GE_07 = Version(pyogrio.__version__) >= Version("0.7.0")
 except ImportError:
     pyogrio = False
     PYOGRIO_GE_07 = False
@@ -65,9 +65,8 @@ def skip_pyogrio_not_supported(engine):
 
 
 @pytest.fixture
-def df_nybb(engine):
-    nybb_path = geopandas.datasets.get_path("nybb")
-    df = read_file(nybb_path, engine=engine)
+def df_nybb(engine, nybb_filename):
+    df = read_file(nybb_filename, engine=engine)
     return df
 
 
@@ -198,9 +197,8 @@ datetime_type_tests = (TEST_DATE, eastern.localize(TEST_DATE))
 @pytest.mark.parametrize("driver,ext", driver_ext_pairs)
 def test_to_file_datetime(tmpdir, driver, ext, time, engine):
     """Test writing a data file with the datetime column type"""
-    if engine == "pyogrio" and time.tzinfo is not None:
-        # TODO
-        pytest.skip("pyogrio doesn't yet support timezones")
+    if engine == "pyogrio" and time.tzinfo is not None and not PYOGRIO_GE_07:
+        pytest.skip("pyogrio < 0.7.0 doesn't support timezones")
     if ext in (".shp", ""):
         pytest.skip(f"Driver corresponding to ext {ext} doesn't support dt fields")
 
@@ -210,23 +208,25 @@ def test_to_file_datetime(tmpdir, driver, ext, time, engine):
     df = GeoDataFrame(
         {"a": [1.0, 2.0], "b": [time, time]}, geometry=[point, point], crs=4326
     )
-    fiona_precision_limit = "ms"
-    df["b"] = df["b"].dt.round(freq=fiona_precision_limit)
+    df["b"] = df["b"].dt.round(freq="ms")
 
     df.to_file(tempfilename, driver=driver, engine=engine)
     df_read = read_file(tempfilename, engine=engine)
 
     assert_geodataframe_equal(df.drop(columns=["b"]), df_read.drop(columns=["b"]))
+    # Check datetime column
+    expected = df["b"]
+    if PANDAS_GE_20:
+        expected = df["b"].dt.as_unit("ms")
+    actual = df_read["b"]
     if df["b"].dt.tz is not None:
         # US/Eastern becomes pytz.FixedOffset(-300) when read from file
-        # so compare fairly in terms of UTC
-        assert_series_equal(
-            df["b"].dt.tz_convert(pytz.utc), df_read["b"].dt.tz_convert(pytz.utc)
-        )
-    else:
-        if engine == "pyogrio" and PANDAS_GE_20:
-            df["b"] = df["b"].astype("datetime64[ms]")
-        assert_series_equal(df["b"], df_read["b"])
+        # as GDAL only models offsets, not timezones.
+        # Compare fair result in terms of UTC instead
+        expected = expected.dt.tz_convert(pytz.utc)
+        actual = actual.dt.tz_convert(pytz.utc)
+
+    assert_series_equal(expected, actual)
 
 
 dt_exts = ["gpkg", "geojson"]
@@ -269,9 +269,6 @@ def test_read_file_datetime_invalid(tmpdir, ext, engine):
 @pytest.mark.parametrize("ext", dt_exts)
 def test_read_file_datetime_out_of_bounds_ns(tmpdir, ext, engine):
     # https://github.com/geopandas/geopandas/issues/2502
-    if ext == "geojson":
-        skip_pyogrio_not_supported(engine)
-
     date_str = "9999-12-31T00:00:00"  # valid to GDAL, not to [ns] format
     tempfilename = write_invalid_date_file(date_str, tmpdir, ext, engine)
     res = read_file(tempfilename)
@@ -295,17 +292,14 @@ def test_read_file_datetime_mixed_offsets(tmpdir):
     df.to_file(tempfilename)
     # check mixed tz don't crash GH2478
     res = read_file(tempfilename)
-    if engine == "fiona":
-        # Convert mixed timezones to UTC equivalent
-        assert is_datetime64_any_dtype(res["date"])
+    # Convert mixed timezones to UTC equivalent
+    assert is_datetime64_any_dtype(res["date"])
+    if engine == "fiona" or (engine == "pyogrio" and PYOGRIO_GE_07):
         if not PANDAS_GE_20:
             utc = pytz.utc
         else:
             utc = datetime.timezone.utc
         assert res["date"].dt.tz == utc
-    else:
-        # old fiona and pyogrio ignore timezones and read as datetimes successfully
-        assert is_datetime64_any_dtype(res["date"])
 
 
 @pytest.mark.parametrize("driver,ext", driver_ext_pairs)
@@ -444,12 +438,12 @@ def test_to_file_schema(tmpdir, df_nybb, engine):
         assert result_schema == schema
 
 
-def test_to_file_crs(tmpdir, engine):
+def test_to_file_crs(tmpdir, engine, nybb_filename):
     """
     Ensure that the file is written according to the crs
     if it is specified
     """
-    df = read_file(geopandas.datasets.get_path("nybb"), engine=engine)
+    df = read_file(nybb_filename, engine=engine)
     tempfilename = os.path.join(str(tmpdir), "crs.shp")
 
     # save correct CRS
@@ -575,8 +569,8 @@ def test_empty_crs(tmpdir, driver, ext, engine):
 NYBB_CRS = "epsg:2263"
 
 
-def test_read_file(engine):
-    df = read_file(geopandas.datasets.get_path("nybb"), engine=engine)
+def test_read_file(engine, nybb_filename):
+    df = read_file(nybb_filename, engine=engine)
     validate_boro_df(df)
     assert df.crs == NYBB_CRS
     expected_columns = ["BoroCode", "BoroName", "Shape_Leng", "Shape_Area"]
@@ -592,7 +586,7 @@ def test_read_file(engine):
         "main/geopandas/tests/data/null_geom.geojson",
         # url to zip file
         "https://raw.githubusercontent.com/geopandas/geopandas/"
-        "main/geopandas/datasets/nybb_16a.zip",
+        "main/geopandas/tests/data/nybb_16a.zip",
         # url to zipfile without extension
         "https://geonode.goosocean.org/download/480",
         # url to web service
@@ -662,11 +656,11 @@ def test_read_file_tempfile(engine):
     temp.close()
 
 
-def test_read_binary_file_fsspec(engine):
+def test_read_binary_file_fsspec(engine, nybb_filename):
     fsspec = pytest.importorskip("fsspec")
     # Remove the zip scheme so fsspec doesn't open as a zipped file,
     # instead we want to read as bytes and let fiona decode it.
-    path = geopandas.datasets.get_path("nybb")[6:]
+    path = nybb_filename[6:]
     with fsspec.open(path, "rb") as f:
         gdf = read_file(f, engine=engine)
         assert isinstance(gdf, geopandas.GeoDataFrame)
@@ -679,10 +673,10 @@ def test_read_text_file_fsspec(file_path, engine):
         assert isinstance(gdf, geopandas.GeoDataFrame)
 
 
-def test_infer_zipped_file(engine):
+def test_infer_zipped_file(engine, nybb_filename):
     # Remove the zip scheme so that the test for a zipped file can
     # check it and add it back.
-    path = geopandas.datasets.get_path("nybb")[6:]
+    path = nybb_filename[6:]
     gdf = read_file(path, engine=engine)
     assert isinstance(gdf, geopandas.GeoDataFrame)
 
@@ -697,15 +691,14 @@ def test_infer_zipped_file(engine):
     assert isinstance(gdf, geopandas.GeoDataFrame)
 
 
-def test_allow_legacy_gdal_path(engine):
+def test_allow_legacy_gdal_path(engine, nybb_filename):
     # Construct a GDAL-style zip path.
-    path = "/vsizip/" + geopandas.datasets.get_path("nybb")[6:]
+    path = "/vsizip/" + nybb_filename[6:]
     gdf = read_file(path, engine=engine)
     assert isinstance(gdf, geopandas.GeoDataFrame)
 
 
-def test_read_file_filtered__bbox(df_nybb, engine):
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def test_read_file_filtered__bbox(df_nybb, engine, nybb_filename):
     bbox = (
         1031051.7879884212,
         224272.49231459625,
@@ -717,8 +710,7 @@ def test_read_file_filtered__bbox(df_nybb, engine):
     assert_geodataframe_equal(filtered_df, expected.reset_index(drop=True))
 
 
-def test_read_file_filtered__bbox__polygon(df_nybb, engine):
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def test_read_file_filtered__bbox__polygon(df_nybb, engine, nybb_filename):
     bbox = box(
         1031051.7879884212, 224272.49231459625, 1047224.3104931959, 244317.30894023244
     )
@@ -727,14 +719,12 @@ def test_read_file_filtered__bbox__polygon(df_nybb, engine):
     assert_geodataframe_equal(filtered_df, expected.reset_index(drop=True))
 
 
-def test_read_file_filtered__rows(df_nybb, engine):
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def test_read_file_filtered__rows(df_nybb, engine, nybb_filename):
     filtered_df = read_file(nybb_filename, rows=1, engine=engine)
     assert_geodataframe_equal(filtered_df, df_nybb.iloc[[0], :])
 
 
-def test_read_file_filtered__rows_slice(df_nybb, engine):
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def test_read_file_filtered__rows_slice(df_nybb, engine, nybb_filename):
     filtered_df = read_file(nybb_filename, rows=slice(1, 3), engine=engine)
     assert_geodataframe_equal(filtered_df, df_nybb.iloc[1:3, :].reset_index(drop=True))
 
@@ -742,8 +732,7 @@ def test_read_file_filtered__rows_slice(df_nybb, engine):
 @pytest.mark.filterwarnings(
     "ignore:Layer does not support OLC_FASTFEATURECOUNT:RuntimeWarning"
 )  # for the slice with -1
-def test_read_file_filtered__rows_bbox(df_nybb, engine):
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def test_read_file_filtered__rows_bbox(df_nybb, engine, nybb_filename):
     bbox = (
         1031051.7879884212,
         224272.49231459625,
@@ -782,16 +771,14 @@ def test_read_file_filtered__rows_bbox(df_nybb, engine):
         )
 
 
-def test_read_file_filtered_rows_invalid(engine):
+def test_read_file_filtered_rows_invalid(engine, nybb_filename):
     with pytest.raises(TypeError):
-        read_file(
-            geopandas.datasets.get_path("nybb"), rows="not_a_slice", engine=engine
-        )
+        read_file(nybb_filename, rows="not_a_slice", engine=engine)
 
 
-def test_read_file__ignore_geometry(engine):
+def test_read_file__ignore_geometry(engine, naturalearth_lowres):
     pdf = geopandas.read_file(
-        geopandas.datasets.get_path("naturalearth_lowres"),
+        naturalearth_lowres,
         ignore_geometry=True,
         engine=engine,
     )
@@ -799,10 +786,10 @@ def test_read_file__ignore_geometry(engine):
     assert isinstance(pdf, pd.DataFrame) and not isinstance(pdf, geopandas.GeoDataFrame)
 
 
-def test_read_file__ignore_all_fields(engine):
+def test_read_file__ignore_all_fields(engine, naturalearth_lowres):
     skip_pyogrio_not_supported(engine)  # pyogrio has "columns" keyword instead
     gdf = geopandas.read_file(
-        geopandas.datasets.get_path("naturalearth_lowres"),
+        naturalearth_lowres,
         ignore_fields=["pop_est", "continent", "name", "iso_a3", "gdp_md_est"],
         engine="fiona",
     )
@@ -836,10 +823,10 @@ def test_read_csv_dtype(tmpdir, df_nybb):
     assert pdf.geometry.dtype == "geometry"
 
 
-def test_read_file__where_filter(engine):
+def test_read_file__where_filter(engine, naturalearth_lowres):
     if FIONA_GE_19 or engine == "pyogrio":
         gdf = geopandas.read_file(
-            geopandas.datasets.get_path("naturalearth_lowres"),
+            naturalearth_lowres,
             where="continent='Africa'",
             engine=engine,
         )
@@ -847,26 +834,25 @@ def test_read_file__where_filter(engine):
     else:
         with pytest.raises(NotImplementedError):
             geopandas.read_file(
-                geopandas.datasets.get_path("naturalearth_lowres"),
+                naturalearth_lowres,
                 where="continent='Africa'",
                 engine="fiona",
             )
 
 
 @PYOGRIO_MARK
-def test_read_file__columns():
+def test_read_file__columns(naturalearth_lowres):
     # TODO: this is only support for pyogrio, but we could mimic it for fiona as well
     gdf = geopandas.read_file(
-        geopandas.datasets.get_path("naturalearth_lowres"),
+        naturalearth_lowres,
         columns=["name", "pop_est"],
         engine="pyogrio",
     )
     assert gdf.columns.tolist() == ["name", "pop_est", "geometry"]
 
 
-def test_read_file_filtered_with_gdf_boundary(df_nybb, engine):
+def test_read_file_filtered_with_gdf_boundary(df_nybb, engine, nybb_filename):
     full_df_shape = df_nybb.shape
-    nybb_filename = geopandas.datasets.get_path("nybb")
     bbox = geopandas.GeoDataFrame(
         geometry=[
             box(
@@ -884,11 +870,13 @@ def test_read_file_filtered_with_gdf_boundary(df_nybb, engine):
     assert filtered_df_shape == (2, 5)
 
 
-def test_read_file_filtered_with_gdf_boundary__mask(df_nybb, engine):
+def test_read_file_filtered_with_gdf_boundary__mask(
+    df_nybb, engine, naturalearth_lowres, naturalearth_cities
+):
     skip_pyogrio_not_supported(engine)
-    gdf_mask = geopandas.read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+    gdf_mask = geopandas.read_file(naturalearth_lowres)
     gdf = geopandas.read_file(
-        geopandas.datasets.get_path("naturalearth_cities"),
+        naturalearth_cities,
         mask=gdf_mask[gdf_mask.continent == "Africa"],
         engine=engine,
     )
@@ -896,10 +884,11 @@ def test_read_file_filtered_with_gdf_boundary__mask(df_nybb, engine):
     assert filtered_df_shape == (57, 2)
 
 
-def test_read_file_filtered_with_gdf_boundary__mask__polygon(df_nybb, engine):
+def test_read_file_filtered_with_gdf_boundary__mask__polygon(
+    df_nybb, engine, nybb_filename
+):
     skip_pyogrio_not_supported(engine)
     full_df_shape = df_nybb.shape
-    nybb_filename = geopandas.datasets.get_path("nybb")
     mask = box(
         1031051.7879884212, 224272.49231459625, 1047224.3104931959, 244317.30894023244
     )
@@ -909,10 +898,11 @@ def test_read_file_filtered_with_gdf_boundary__mask__polygon(df_nybb, engine):
     assert filtered_df_shape == (2, 5)
 
 
-def test_read_file_filtered_with_gdf_boundary_mismatched_crs(df_nybb, engine):
+def test_read_file_filtered_with_gdf_boundary_mismatched_crs(
+    df_nybb, engine, nybb_filename
+):
     skip_pyogrio_not_supported(engine)
     full_df_shape = df_nybb.shape
-    nybb_filename = geopandas.datasets.get_path("nybb")
     bbox = geopandas.GeoDataFrame(
         geometry=[
             box(
@@ -931,10 +921,11 @@ def test_read_file_filtered_with_gdf_boundary_mismatched_crs(df_nybb, engine):
     assert filtered_df_shape == (2, 5)
 
 
-def test_read_file_filtered_with_gdf_boundary_mismatched_crs__mask(df_nybb, engine):
+def test_read_file_filtered_with_gdf_boundary_mismatched_crs__mask(
+    df_nybb, engine, nybb_filename
+):
     skip_pyogrio_not_supported(engine)
     full_df_shape = df_nybb.shape
-    nybb_filename = geopandas.datasets.get_path("nybb")
     mask = geopandas.GeoDataFrame(
         geometry=[
             box(
@@ -983,9 +974,9 @@ def test_read_file_empty_shapefile(tmpdir, engine):
     assert all(empty.columns == ["A", "Z", "geometry"])
 
 
-def test_read_file_privacy(tmpdir, df_nybb):
+def test_read_file_privacy(tmpdir, df_nybb, nybb_filename):
     with pytest.warns(FutureWarning):
-        geopandas.io.file.read_file(geopandas.datasets.get_path("nybb"))
+        geopandas.io.file.read_file(nybb_filename)
 
 
 class FileNumber(object):
@@ -1154,7 +1145,7 @@ def test_write_index_to_file(tmpdir, df_points, driver, ext, engine):
     # index as string
     df_p = df_points.copy()
     df = GeoDataFrame(df_p["value1"], geometry=df_p.geometry)
-    df.index = pd.TimedeltaIndex(range(len(df)), "days")
+    df.index = pd.to_timedelta(range(len(df)), unit="days")
     # TODO: TimedeltaIndex is an invalid field type
     df.index = df.index.astype(str)
     do_checks(df, index_is_used=True)
@@ -1162,7 +1153,7 @@ def test_write_index_to_file(tmpdir, df_points, driver, ext, engine):
     # unnamed DatetimeIndex
     df_p = df_points.copy()
     df = GeoDataFrame(df_p["value1"], geometry=df_p.geometry)
-    df.index = pd.TimedeltaIndex(range(len(df)), "days") + pd.DatetimeIndex(
+    df.index = pd.to_timedelta(range(len(df)), unit="days") + pd.to_datetime(
         ["1999-12-27"] * len(df)
     )
     if driver == "ESRI Shapefile":
@@ -1201,7 +1192,7 @@ def test_multiple_geom_cols_error(tmpdir, df_nybb):
 
 @PYOGRIO_MARK
 @FIONA_MARK
-def test_option_io_engine():
+def test_option_io_engine(nybb_filename):
     try:
         geopandas.options.io_engine = "pyogrio"
 
@@ -1212,7 +1203,6 @@ def test_option_io_engine():
         orig = fiona.supported_drivers["ESRI Shapefile"]
         fiona.supported_drivers["ESRI Shapefile"] = "w"
 
-        nybb_filename = geopandas.datasets.get_path("nybb")
         _ = geopandas.read_file(nybb_filename)
     finally:
         fiona.supported_drivers["ESRI Shapefile"] = orig
diff --git a/geopandas/io/tests/test_file_geom_types_drivers.py b/geopandas/io/tests/test_file_geom_types_drivers.py
index fc873dab..d500d964 100644
--- a/geopandas/io/tests/test_file_geom_types_drivers.py
+++ b/geopandas/io/tests/test_file_geom_types_drivers.py
@@ -244,7 +244,14 @@ def geodataframe(request):
     return request.param
 
 
-@pytest.fixture(params=["GeoJSON", "ESRI Shapefile", "GPKG", "SQLite"])
+@pytest.fixture(
+    params=[
+        ("GeoJSON", ".geojson"),
+        ("ESRI Shapefile", ".shp"),
+        ("GPKG", ".gpkg"),
+        ("SQLite", ".sqlite"),
+    ]
+)
 def ogr_driver(request):
     return request.param
 
@@ -260,9 +267,10 @@ def engine(request):
 
 
 def test_to_file_roundtrip(tmpdir, geodataframe, ogr_driver, engine):
-    output_file = os.path.join(str(tmpdir), "output_file")
+    driver, ext = ogr_driver
+    output_file = os.path.join(str(tmpdir), "output_file" + ext)
     write_kwargs = {}
-    if ogr_driver == "SQLite":
+    if driver == "SQLite":
         write_kwargs["spatialite"] = True
 
         # This if statement can be removed once minimal fiona version >= 1.8.20
@@ -285,22 +293,20 @@ def test_to_file_roundtrip(tmpdir, geodataframe, ogr_driver, engine):
         ):
             write_kwargs["geometry_type"] = "Point Z"
 
-    expected_error = _expected_error_on(geodataframe, ogr_driver)
+    expected_error = _expected_error_on(geodataframe, driver)
     if expected_error:
         with pytest.raises(
             RuntimeError, match="Failed to write record|Could not add feature to layer"
         ):
             geodataframe.to_file(
-                output_file, driver=ogr_driver, engine=engine, **write_kwargs
+                output_file, driver=driver, engine=engine, **write_kwargs
             )
     else:
-        geodataframe.to_file(
-            output_file, driver=ogr_driver, engine=engine, **write_kwargs
-        )
+        geodataframe.to_file(output_file, driver=driver, engine=engine, **write_kwargs)
 
         reloaded = geopandas.read_file(output_file, engine=engine)
 
-        if ogr_driver == "GeoJSON" and engine == "pyogrio":
+        if driver == "GeoJSON" and engine == "pyogrio":
             # For GeoJSON files, the int64 column comes back as int32
             reloaded["a"] = reloaded["a"].astype("int64")
 
diff --git a/geopandas/io/tests/test_sql.py b/geopandas/io/tests/test_sql.py
index a072a481..8a5a7fc5 100644
--- a/geopandas/io/tests/test_sql.py
+++ b/geopandas/io/tests/test_sql.py
@@ -26,9 +26,8 @@ except ImportError:
 
 
 @pytest.fixture
-def df_nybb():
-    nybb_path = geopandas.datasets.get_path("nybb")
-    df = read_file(nybb_path)
+def df_nybb(nybb_filename):
+    df = read_file(nybb_filename)
     return df
 
 
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.VERSION.txt b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.VERSION.txt
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.VERSION.txt
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.VERSION.txt
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.cpg b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.cpg
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.cpg
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.cpg
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.dbf b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.dbf
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.dbf
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.dbf
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.prj b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.prj
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.prj
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.prj
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.shp b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.shp
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.shp
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.shp
diff --git a/geopandas/datasets/naturalearth_cities/naturalearth_cities.shx b/geopandas/tests/data/naturalearth_cities/naturalearth_cities.shx
similarity index 100%
rename from geopandas/datasets/naturalearth_cities/naturalearth_cities.shx
rename to geopandas/tests/data/naturalearth_cities/naturalearth_cities.shx
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.cpg b/geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.cpg
similarity index 100%
rename from geopandas/datasets/naturalearth_lowres/naturalearth_lowres.cpg
rename to geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.cpg
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.dbf b/geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.dbf
similarity index 100%
rename from geopandas/datasets/naturalearth_lowres/naturalearth_lowres.dbf
rename to geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.dbf
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.prj b/geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.prj
similarity index 100%
rename from geopandas/datasets/naturalearth_lowres/naturalearth_lowres.prj
rename to geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.prj
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shp b/geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.shp
similarity index 100%
rename from geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shp
rename to geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.shp
diff --git a/geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shx b/geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.shx
similarity index 100%
rename from geopandas/datasets/naturalearth_lowres/naturalearth_lowres.shx
rename to geopandas/tests/data/naturalearth_lowres/naturalearth_lowres.shx
diff --git a/geopandas/datasets/nybb_16a.zip b/geopandas/tests/data/nybb_16a.zip
similarity index 100%
rename from geopandas/datasets/nybb_16a.zip
rename to geopandas/tests/data/nybb_16a.zip
diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py
index 6a30b5ac..34ae3f74 100644
--- a/geopandas/tests/test_array.py
+++ b/geopandas/tests/test_array.py
@@ -301,9 +301,9 @@ def test_as_array():
         ("geom_almost_equals", (3,)),
     ],
 )
-@pytest.mark.filterwarnings(
-    r"ignore:The \'almost_equals\(\)\' method is deprecated"
-)  # filter required for attr=geom_almost_equals only
+# filters required for attr=geom_almost_equals only
+@pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
+@pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
 def test_predicates_vector_scalar(attr, args):
     na_value = False
 
@@ -343,9 +343,9 @@ def test_predicates_vector_scalar(attr, args):
         ("geom_almost_equals", (3,)),
     ],
 )
-@pytest.mark.filterwarnings(
-    r"ignore:The \'almost_equals\(\)\' method is deprecated"
-)  # filter required for attr=geom_almost_equals only
+# filters required for attr=geom_almost_equals only
+@pytest.mark.filterwarnings(r"ignore:The \'geom_almost_equals\(\)\' method is deprecat")
+@pytest.mark.filterwarnings(r"ignore:The \'almost_equals\(\)\' method is deprecated")
 def test_predicates_vector_vector(attr, args):
     na_value = False
     empty_value = True if attr == "disjoint" else False
diff --git a/geopandas/tests/test_crs.py b/geopandas/tests/test_crs.py
index e2c7b13f..ed49834b 100644
--- a/geopandas/tests/test_crs.py
+++ b/geopandas/tests/test_crs.py
@@ -7,7 +7,7 @@ import pyproj
 import pytest
 from shapely.geometry import Point, Polygon, LineString
 
-from geopandas import GeoSeries, GeoDataFrame, points_from_xy, datasets, read_file
+from geopandas import GeoSeries, GeoDataFrame, points_from_xy, read_file
 from geopandas.array import from_shapely, from_wkb, from_wkt, GeometryArray
 from geopandas.testing import assert_geodataframe_equal
 
@@ -83,6 +83,9 @@ def test_to_crs_dimension_z():
     assert result.has_z.all()
 
 
+# pyproj + numpy 1.25 trigger warning for single-element array -> recommdation is to
+# ignore the warning for now (https://github.com/pyproj4/pyproj/issues/1307)
+@pytest.mark.filterwarnings("ignore:Conversion of an array with:DeprecationWarning")
 def test_to_crs_dimension_mixed():
     s = GeoSeries([Point(1, 2), LineString([(1, 2, 3), (4, 5, 6)])], crs=2056)
     result = s.to_crs(epsg=4326)
@@ -151,6 +154,9 @@ def test_transform2(epsg4326, epsg26918):
     assert_geodataframe_equal(df, utm, check_less_precise=True, check_crs=False)
 
 
+# pyproj + numpy 1.25 trigger warning for single-element array -> recommdation is to
+# ignore the warning for now (https://github.com/pyproj4/pyproj/issues/1307)
+@pytest.mark.filterwarnings("ignore:Conversion of an array with:DeprecationWarning")
 def test_crs_axis_order__always_xy():
     df = GeoDataFrame(geometry=[Point(-1683723, 6689139)], crs="epsg:26918")
     lonlat = df.to_crs("epsg:4326")
@@ -420,8 +426,7 @@ class TestGeometryArrayCRS:
             df = GeoDataFrame()
             df.crs = 4326
 
-    def test_read_file(self):
-        nybb_filename = datasets.get_path("nybb")
+    def test_read_file(self, nybb_filename):
         df = read_file(nybb_filename)
         assert df.crs == pyproj.CRS(2263)
         assert df.geometry.crs == pyproj.CRS(2263)
diff --git a/geopandas/tests/test_datasets.py b/geopandas/tests/test_datasets.py
index 2b4b14a9..09dde905 100644
--- a/geopandas/tests/test_datasets.py
+++ b/geopandas/tests/test_datasets.py
@@ -5,8 +5,11 @@ import pytest
 
 
 @pytest.mark.parametrize(
-    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb"]
+    "test_dataset", ["naturalearth_lowres", "naturalearth_cities", "nybb", "foo"]
 )
 def test_read_paths(test_dataset):
-    with pytest.warns(FutureWarning, match="The geopandas.dataset module is"):
+    with pytest.raises(
+        AttributeError,
+        match=r"The geopandas\.dataset has been deprecated and was removed",
+    ):
         assert isinstance(read_file(get_path(test_dataset)), GeoDataFrame)
diff --git a/geopandas/tests/test_dissolve.py b/geopandas/tests/test_dissolve.py
index d16b295a..68412248 100644
--- a/geopandas/tests/test_dissolve.py
+++ b/geopandas/tests/test_dissolve.py
@@ -14,8 +14,7 @@ from geopandas.testing import assert_geodataframe_equal, geom_almost_equals
 
 
 @pytest.fixture
-def nybb_polydf():
-    nybb_filename = geopandas.datasets.get_path("nybb")
+def nybb_polydf(nybb_filename):
     nybb_polydf = read_file(nybb_filename)
     nybb_polydf = nybb_polydf[["geometry", "BoroName", "BoroCode"]]
     nybb_polydf = nybb_polydf.rename(columns={"geometry": "myshapes"})
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 9e134a7b..111c736f 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -19,18 +19,27 @@ BRANCA_05 = Version(branca.__version__) > Version("0.4.2")
 FOLIUM_G_014 = Version(folium.__version__) > Version("0.14.0")
 
 
+@pytest.fixture(scope="class")
+def _setup_class_test_explore(
+    nybb_filename, naturalearth_lowres, naturalearth_cities, request
+):
+    request.cls.nybb = gpd.read_file(nybb_filename)
+    request.cls.world = gpd.read_file(naturalearth_lowres)
+    request.cls.cities = gpd.read_file(naturalearth_cities)
+    request.cls.chicago = gpd.read_file(geodatasets.get_path("geoda.chicago_commpop"))
+    request.cls.world["range"] = range(len(request.cls.world))
+    request.cls.missing = request.cls.world.copy()
+    np.random.seed(42)
+    request.cls.missing.loc[
+        np.random.choice(request.cls.missing.index, 40), "continent"
+    ] = np.nan
+    request.cls.missing.loc[
+        np.random.choice(request.cls.missing.index, 40), "pop_est"
+    ] = np.nan
+
+
+@pytest.mark.usefixtures("_setup_class_test_explore")
 class TestExplore:
-    def setup_method(self):
-        self.nybb = gpd.read_file(gpd.datasets.get_path("nybb"))
-        self.world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
-        self.cities = gpd.read_file(gpd.datasets.get_path("naturalearth_cities"))
-        self.chicago = gpd.read_file(geodatasets.get_path("geoda.chicago_commpop"))
-        self.world["range"] = range(len(self.world))
-        self.missing = self.world.copy()
-        np.random.seed(42)
-        self.missing.loc[np.random.choice(self.missing.index, 40), "continent"] = np.nan
-        self.missing.loc[np.random.choice(self.missing.index, 40), "pop_est"] = np.nan
-
     def _fetch_map_string(self, m):
         out = m._parent.render()
         out_str = "".join(out.split())
diff --git a/geopandas/tests/test_geodataframe.py b/geopandas/tests/test_geodataframe.py
index ea56ebeb..85cb3c56 100644
--- a/geopandas/tests/test_geodataframe.py
+++ b/geopandas/tests/test_geodataframe.py
@@ -8,7 +8,7 @@ import pandas as pd
 
 from pyproj import CRS
 from pyproj.exceptions import CRSError
-from shapely.geometry import Point, Polygon
+from shapely.geometry import Point, Polygon, box
 
 import geopandas
 from geopandas import GeoDataFrame, GeoSeries, points_from_xy, read_file
@@ -17,6 +17,7 @@ from geopandas.array import GeometryArray, GeometryDtype, from_shapely
 from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
 from geopandas.tests.util import PACKAGE_DIR, validate_boro_df
 from pandas.testing import assert_frame_equal, assert_index_equal, assert_series_equal
+import geopandas._compat as compat
 import pytest
 
 
@@ -46,12 +47,13 @@ def how(request):
     return request.param
 
 
+@pytest.mark.usefixtures("_setup_class_nybb_filename")
 class TestDataFrame:
     def setup_method(self):
         N = 10
-
-        nybb_filename = geopandas.datasets.get_path("nybb")
-        self.df = read_file(nybb_filename)
+        # self.nybb_filename attached via _setup_class_nybb_filename
+        self.df = read_file(self.nybb_filename)
+        # TODO re-write instance variables to be fixtures
         self.tempdir = tempfile.mkdtemp()
         self.crs = "epsg:4326"
         self.df2 = GeoDataFrame(
@@ -363,6 +365,21 @@ class TestDataFrame:
         with pytest.raises(AttributeError, match=msg_geo_col_missing):
             df.geometry
 
+    def test_active_geometry_name(self):
+        # default single active called "geometry"
+        assert self.df.active_geometry_name == "geometry"
+
+        # one GeoSeries, not active
+        no_active = GeoDataFrame({"foo": self.df.BoroName, "bar": self.df.geometry})
+        assert no_active.active_geometry_name is None
+        assert no_active.set_geometry("bar").active_geometry_name == "bar"
+
+        # multiple, none active
+        multiple = GeoDataFrame({"foo": self.df.geometry, "bar": self.df.geometry})
+        assert multiple.active_geometry_name is None
+        assert multiple.set_geometry("foo").active_geometry_name == "foo"
+        assert multiple.set_geometry("bar").active_geometry_name == "bar"
+
     def test_align(self):
         df = self.df2
 
@@ -588,9 +605,8 @@ class TestDataFrame:
         df = GeoDataFrame.from_dict(data, geometry="location")
         assert df._geometry_column_name == "location"
 
-    def test_from_features(self):
+    def test_from_features(self, nybb_filename):
         fiona = pytest.importorskip("fiona")
-        nybb_filename = geopandas.datasets.get_path("nybb")
         with fiona.open(nybb_filename) as f:
             features = list(f)
             crs = f.crs_wkt
@@ -965,29 +981,51 @@ class TestDataFrame:
 
     @pytest.mark.parametrize("how", ["left", "inner", "right"])
     @pytest.mark.parametrize("predicate", ["intersects", "within", "contains"])
-    def test_sjoin(self, how, predicate):
+    def test_sjoin(self, how, predicate, naturalearth_cities, naturalearth_lowres):
         """
         Basic test for availability of the GeoDataFrame method. Other
         sjoin tests are located in /tools/tests/test_sjoin.py
         """
-        left = read_file(geopandas.datasets.get_path("naturalearth_cities"))
-        right = read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+        left = read_file(naturalearth_cities)
+        right = read_file(naturalearth_lowres)
 
         expected = geopandas.sjoin(left, right, how=how, predicate=predicate)
         result = left.sjoin(right, how=how, predicate=predicate)
         assert_geodataframe_equal(result, expected)
 
+    @pytest.mark.parametrize("how", ["left", "inner", "right"])
+    @pytest.mark.parametrize("distance", [0, 3])
+    @pytest.mark.skipif(
+        not compat.GEOS_GE_310,
+        reason="`dwithin` requires GEOS 3.10",
+    )
+    def test_sjoin_dwithin(self, how, distance):
+        """
+        Basic test for predicate='dwithin' availability of the GeoDataFrame method.
+        Other sjoin tests are located in /tools/tests/test_sjoin.py
+        """
+        left = GeoDataFrame(geometry=points_from_xy([0, 1, 2], [0, 1, 1]))
+        right = GeoDataFrame(geometry=[box(0, 0, 1, 1)])
+
+        expected = geopandas.sjoin(
+            left, right, how=how, predicate="dwithin", distance=distance
+        )
+        result = left.sjoin(right, how=how, predicate="dwithin", distance=distance)
+        assert_geodataframe_equal(result, expected)
+
     @pytest.mark.parametrize("how", ["left", "inner", "right"])
     @pytest.mark.parametrize("max_distance", [None, 1])
     @pytest.mark.parametrize("distance_col", [None, "distance"])
     @pytest.mark.filterwarnings("ignore:Geometry is in a geographic CRS:UserWarning")
-    def test_sjoin_nearest(self, how, max_distance, distance_col):
+    def test_sjoin_nearest(
+        self, how, max_distance, distance_col, naturalearth_cities, naturalearth_lowres
+    ):
         """
         Basic test for availability of the GeoDataFrame method. Other
         sjoin tests are located in /tools/tests/test_sjoin.py
         """
-        left = read_file(geopandas.datasets.get_path("naturalearth_cities"))
-        right = read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+        left = read_file(naturalearth_cities)
+        right = read_file(naturalearth_lowres)
 
         expected = geopandas.sjoin_nearest(
             left, right, how=how, max_distance=max_distance, distance_col=distance_col
@@ -997,13 +1035,13 @@ class TestDataFrame:
         )
         assert_geodataframe_equal(result, expected)
 
-    def test_clip(self):
+    def test_clip(self, naturalearth_cities, naturalearth_lowres):
         """
         Basic test for availability of the GeoDataFrame method. Other
         clip tests are located in /tools/tests/test_clip.py
         """
-        left = read_file(geopandas.datasets.get_path("naturalearth_cities"))
-        world = read_file(geopandas.datasets.get_path("naturalearth_lowres"))
+        left = read_file(naturalearth_cities)
+        world = read_file(naturalearth_lowres)
         south_america = world[world["continent"] == "South America"]
 
         expected = geopandas.clip(left, south_america)
@@ -1388,8 +1426,7 @@ class TestConstructor:
         assert gdf._geometry_column_name == ("geometry", "", "")
         assert gdf.geometry.name == ("geometry", "", "")
 
-    def test_assign_cols_using_index(self):
-        nybb_filename = geopandas.datasets.get_path("nybb")
+    def test_assign_cols_using_index(self, nybb_filename):
         df = read_file(nybb_filename)
         other_df = pd.DataFrame({"foo": range(5), "bar": range(5)})
         expected = pd.concat([df, other_df], axis=1)
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 8eee1b4b..72f75f77 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -1254,7 +1254,7 @@ class TestGeomMethods:
     def test_minimum_bounding_circle(self):
         mbc = self.g1.minimum_bounding_circle()
         centers = GeoSeries([Point(0.5, 0.5)] * 2)
-        assert np.all(mbc.centroid.geom_almost_equals(centers, 0.001))
+        assert np.all(mbc.centroid.geom_equals_exact(centers, 0.001))
         assert_series_equal(
             mbc.area,
             Series([1.560723, 1.560723]),
diff --git a/geopandas/tests/test_geoseries.py b/geopandas/tests/test_geoseries.py
index e72a9b66..4ebf2463 100644
--- a/geopandas/tests/test_geoseries.py
+++ b/geopandas/tests/test_geoseries.py
@@ -22,7 +22,7 @@ from shapely.geometry import (
 )
 from shapely.geometry.base import BaseGeometry
 
-from geopandas import GeoSeries, GeoDataFrame, read_file, datasets, clip
+from geopandas import GeoSeries, GeoDataFrame, read_file, clip
 from geopandas.array import GeometryArray, GeometryDtype
 from geopandas.testing import assert_geoseries_equal, geom_almost_equals
 
@@ -362,9 +362,9 @@ class TestSeries:
     def test_to_wkt(self):
         assert_series_equal(pd.Series([self.t1.wkt, self.sq.wkt]), self.g1.to_wkt())
 
-    def test_clip(self):
-        left = read_file(datasets.get_path("naturalearth_cities"))
-        world = read_file(datasets.get_path("naturalearth_lowres"))
+    def test_clip(self, naturalearth_lowres, naturalearth_cities):
+        left = read_file(naturalearth_cities)
+        world = read_file(naturalearth_lowres)
         south_america = world[world["continent"] == "South America"]
 
         expected = clip(left.geometry, south_america)
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
index 30d24645..8e9dae0d 100644
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -113,8 +113,8 @@ def test_overlay(dfs_index, how):
 
 
 @pytest.mark.filterwarnings("ignore:GeoSeries crs mismatch:UserWarning")
-def test_overlay_nybb(how):
-    polydf = read_file(geopandas.datasets.get_path("nybb"))
+def test_overlay_nybb(how, nybb_filename):
+    polydf = read_file(nybb_filename)
 
     # The circles have been constructed and saved at the time the expected
     # results were created (exact output of buffer algorithm can slightly
@@ -712,8 +712,8 @@ def test_overlap_make_valid(should_make_valid):
             overlay(df1, df_bowtie, make_valid=should_make_valid)
 
 
-def test_empty_overlay_return_non_duplicated_columns():
-    nybb = geopandas.read_file(geopandas.datasets.get_path("nybb"))
+def test_empty_overlay_return_non_duplicated_columns(nybb_filename):
+    nybb = geopandas.read_file(nybb_filename)
     nybb2 = nybb.copy()
     nybb2.geometry = nybb2.translate(20000000)
 
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 16b304c1..34bd9ddc 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -657,27 +657,47 @@ def test_groupby_groups(df):
 
 
 @pytest.mark.parametrize("crs", [None, "EPSG:4326"])
-def test_groupby_metadata(crs):
+@pytest.mark.parametrize("geometry_name", ["geometry", "geom"])
+def test_groupby_metadata(crs, geometry_name):
     # https://github.com/geopandas/geopandas/issues/2294
     df = GeoDataFrame(
         {
-            "geometry": [Point(0, 0), Point(1, 1), Point(0, 0)],
+            geometry_name: [Point(0, 0), Point(1, 1), Point(0, 0)],
             "value1": np.arange(3, dtype="int64"),
             "value2": np.array([1, 2, 1], dtype="int64"),
         },
         crs=crs,
+        geometry=geometry_name,
     )
 
+    kwargs = {}
+    if compat.PANDAS_GE_22:
+        # pandas is deprecating that the group key is present as column in the
+        # dataframe passed to `func`. To suppress this warning, it introduced
+        # a new include_groups keyword
+        kwargs = dict(include_groups=False)
+
     # dummy test asserting we can access the crs
     def func(group):
         assert isinstance(group, GeoDataFrame)
         assert group.crs == crs
 
-    df.groupby("value2").apply(func)
+    df.groupby("value2").apply(func, **kwargs)
+    # selecting the non-group columns -> no need to pass the keyword
+    if (
+        compat.PANDAS_GE_21 if geometry_name == "geometry" else compat.PANDAS_GE_20
+    ) and not compat.PANDAS_GE_22:
+        # https://github.com/geopandas/geopandas/pull/2966#issuecomment-1878816712
+        # with pandas 2.0 and 2.1 this is failing
+        with pytest.raises(AttributeError):
+            df.groupby("value2")[[geometry_name, "value1"]].apply(func)
+    else:
+        df.groupby("value2")[[geometry_name, "value1"]].apply(func)
 
     # actual test with functionality
     res = df.groupby("value2").apply(
-        lambda x: geopandas.sjoin(x, x[["geometry", "value1"]], how="inner")
+        lambda x: geopandas.sjoin(x, x[[geometry_name, "value1"]], how="inner"),
+        **kwargs,
     )
 
     if compat.PANDAS_GE_22:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 2477989b..4ecb4ab0 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -20,7 +20,6 @@ from shapely.geometry import (
 
 
 from geopandas import GeoDataFrame, GeoSeries, read_file
-from geopandas.datasets import get_path
 import geopandas._compat as compat
 from geopandas.plotting import GeoplotAccessor
 
@@ -1068,16 +1067,19 @@ class TestNonuniformGeometryPlotting:
         #     )
 
 
-class TestGeographicAspect:
-    def setup_class(self):
-        pth = get_path("naturalearth_lowres")
-        df = read_file(pth)
-        self.north = df.loc[df.continent == "North America"]
-        self.north_proj = self.north.to_crs("ESRI:102008")
-        bounds = self.north.total_bounds
-        y_coord = np.mean([bounds[1], bounds[3]])
-        self.exp = 1 / np.cos(y_coord * np.pi / 180)
+@pytest.fixture(scope="class")
+def _setup_class_geographic_aspect(naturalearth_lowres, request):
+    """Attach naturalearth_lowres class attribute for unittest style setup_method"""
+    df = read_file(naturalearth_lowres)
+    request.cls.north = df.loc[df.continent == "North America"]
+    request.cls.north_proj = request.cls.north.to_crs("ESRI:102008")
+    bounds = request.cls.north.total_bounds
+    y_coord = np.mean([bounds[1], bounds[3]])
+    request.cls.exp = 1 / np.cos(y_coord * np.pi / 180)
+
 
+@pytest.mark.usefixtures("_setup_class_geographic_aspect")
+class TestGeographicAspect:
     def test_auto(self):
         ax = self.north.geometry.plot()
         assert ax.get_aspect() == self.exp
@@ -1131,6 +1133,9 @@ class TestGeographicAspect:
         assert ax3.get_aspect() == 0.5
 
 
+@pytest.mark.filterwarnings(
+    "ignore:Numba not installed. Using slow pure python version.:UserWarning"
+)
 class TestMapclassifyPlotting:
     @classmethod
     def setup_class(cls):
@@ -1141,31 +1146,40 @@ class TestMapclassifyPlotting:
         cls.mc = mapclassify
         cls.classifiers = list(mapclassify.classifiers.CLASSIFIERS)
         cls.classifiers.remove("UserDefined")
-        pth = get_path("naturalearth_lowres")
-        cls.df = read_file(pth)
-        cls.df["NEGATIVES"] = np.linspace(-10, 10, len(cls.df.index))
-        cls.df["low_vals"] = np.linspace(0, 0.3, cls.df.shape[0])
-        cls.df["mid_vals"] = np.linspace(0.3, 0.7, cls.df.shape[0])
-        cls.df["high_vals"] = np.linspace(0.7, 1.0, cls.df.shape[0])
-        cls.df.loc[cls.df.index[:20:2], "high_vals"] = np.nan
-        cls.nybb = read_file(get_path("nybb"))
-        cls.nybb["vals"] = [0.001, 0.002, 0.003, 0.004, 0.005]
 
-    def test_legend(self):
+    @pytest.fixture
+    def df(self, naturalearth_lowres):
+        # version of naturalearth_lowres for mapclassify plotting tests
+        df = read_file(naturalearth_lowres)
+        df["NEGATIVES"] = np.linspace(-10, 10, len(df.index))
+        df["low_vals"] = np.linspace(0, 0.3, df.shape[0])
+        df["mid_vals"] = np.linspace(0.3, 0.7, df.shape[0])
+        df["high_vals"] = np.linspace(0.7, 1.0, df.shape[0])
+        df.loc[df.index[:20:2], "high_vals"] = np.nan
+        return df
+
+    @pytest.fixture
+    def nybb(self, nybb_filename):
+        # version of nybb for mapclassify plotting tests
+        df = read_file(nybb_filename)
+        df["vals"] = [0.001, 0.002, 0.003, 0.004, 0.005]
+        return df
+
+    def test_legend(self, df):
         with warnings.catch_warnings(record=True) as _:  # don't print warning
             # warning coming from scipy.stats
-            ax = self.df.plot(
+            ax = df.plot(
                 column="pop_est", scheme="QUANTILES", k=3, cmap="OrRd", legend=True
             )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = [
             s.split("|")[0][1:-2]
-            for s in str(self.mc.Quantiles(self.df["pop_est"], k=3)).split("\n")[4:]
+            for s in str(self.mc.Quantiles(df["pop_est"], k=3)).split("\n")[4:]
         ]
         assert labels == expected
 
-    def test_bin_labels(self):
-        ax = self.df.plot(
+    def test_bin_labels(self, df):
+        ax = df.plot(
             column="pop_est",
             scheme="QUANTILES",
             k=3,
@@ -1177,9 +1191,9 @@ class TestMapclassifyPlotting:
         expected = ["foo", "bar", "baz"]
         assert labels == expected
 
-    def test_invalid_labels_length(self):
+    def test_invalid_labels_length(self, df):
         with pytest.raises(ValueError):
-            self.df.plot(
+            df.plot(
                 column="pop_est",
                 scheme="QUANTILES",
                 k=3,
@@ -1188,16 +1202,16 @@ class TestMapclassifyPlotting:
                 legend_kwds={"labels": ["foo", "bar"]},
             )
 
-    def test_negative_legend(self):
-        ax = self.df.plot(
+    def test_negative_legend(self, df):
+        ax = df.plot(
             column="NEGATIVES", scheme="FISHER_JENKS", k=3, cmap="OrRd", legend=True
         )
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = ["-10.00,  -3.41", " -3.41,   3.30", "  3.30,  10.00"]
         assert labels == expected
 
-    def test_fmt(self):
-        ax = self.df.plot(
+    def test_fmt(self, df):
+        ax = df.plot(
             column="NEGATIVES",
             scheme="FISHER_JENKS",
             k=3,
@@ -1209,8 +1223,8 @@ class TestMapclassifyPlotting:
         expected = ["-10,  -3", " -3,   3", "  3,  10"]
         assert labels == expected
 
-    def test_interval(self):
-        ax = self.df.plot(
+    def test_interval(self, df):
+        ax = df.plot(
             column="NEGATIVES",
             scheme="FISHER_JENKS",
             k=3,
@@ -1223,17 +1237,17 @@ class TestMapclassifyPlotting:
         assert labels == expected
 
     @pytest.mark.parametrize("scheme", ["FISHER_JENKS", "FISHERJENKS"])
-    def test_scheme_name_compat(self, scheme):
-        ax = self.df.plot(column="NEGATIVES", scheme=scheme, k=3, legend=True)
+    def test_scheme_name_compat(self, scheme, df):
+        ax = df.plot(column="NEGATIVES", scheme=scheme, k=3, legend=True)
         assert len(ax.get_legend().get_texts()) == 3
 
-    def test_schemes(self):
+    def test_schemes(self, df):
         # test if all available classifiers pass
         for scheme in self.classifiers:
-            self.df.plot(column="pop_est", scheme=scheme, legend=True)
+            df.plot(column="pop_est", scheme=scheme, legend=True)
 
-    def test_classification_kwds(self):
-        ax = self.df.plot(
+    def test_classification_kwds(self, df):
+        ax = df.plot(
             column="pop_est",
             scheme="percentiles",
             k=3,
@@ -1244,21 +1258,19 @@ class TestMapclassifyPlotting:
         labels = [t.get_text() for t in ax.get_legend().get_texts()]
         expected = [
             s.split("|")[0][1:-2]
-            for s in str(self.mc.Percentiles(self.df["pop_est"], pct=[50, 100])).split(
-                "\n"
-            )[4:]
+            for s in str(self.mc.Percentiles(df["pop_est"], pct=[50, 100])).split("\n")[
+                4:
+            ]
         ]
 
         assert labels == expected
 
-    def test_invalid_scheme(self):
+    def test_invalid_scheme(self, df):
         with pytest.raises(ValueError):
             scheme = "invalid_scheme_*#&)(*#"
-            self.df.plot(
-                column="gdp_md_est", scheme=scheme, k=3, cmap="OrRd", legend=True
-            )
+            df.plot(column="gdp_md_est", scheme=scheme, k=3, cmap="OrRd", legend=True)
 
-    def test_cax_legend_passing(self):
+    def test_cax_legend_passing(self, df):
         """Pass a 'cax' argument to 'df.plot(.)', that is valid only if 'ax' is
         passed as well (if not, a new figure is created ad hoc, and 'cax' is
         ignored)
@@ -1269,15 +1281,15 @@ class TestMapclassifyPlotting:
         divider = make_axes_locatable(ax)
         cax = divider.append_axes("right", size="5%", pad=0.1)
         with pytest.raises(ValueError):
-            ax = self.df.plot(column="pop_est", cmap="OrRd", legend=True, cax=cax)
+            ax = df.plot(column="pop_est", cmap="OrRd", legend=True, cax=cax)
 
-    def test_cax_legend_height(self):
+    def test_cax_legend_height(self, df):
         """Pass a cax argument to 'df.plot(.)', the legend location must be
         aligned with those of main plot
         """
         # base case
         with warnings.catch_warnings(record=True) as _:  # don't print warning
-            ax = self.df.plot(column="pop_est", cmap="OrRd", legend=True)
+            ax = df.plot(column="pop_est", cmap="OrRd", legend=True)
         plot_height = _get_ax(ax.get_figure(), "").get_position().height
         legend_height = _get_ax(ax.get_figure(), "<colorbar>").get_position().height
         assert abs(plot_height - legend_height) >= 1e-6
@@ -1288,16 +1300,14 @@ class TestMapclassifyPlotting:
         divider = make_axes_locatable(ax2)
         cax = divider.append_axes("right", size="5%", pad=0.1, label="fixed_colorbar")
         with warnings.catch_warnings(record=True) as _:
-            ax2 = self.df.plot(
-                column="pop_est", cmap="OrRd", legend=True, cax=cax, ax=ax2
-            )
+            ax2 = df.plot(column="pop_est", cmap="OrRd", legend=True, cax=cax, ax=ax2)
         plot_height = _get_ax(fig, "").get_position().height
         legend_height = _get_ax(fig, "fixed_colorbar").get_position().height
         assert abs(plot_height - legend_height) < 1e-6
 
-    def test_empty_bins(self):
+    def test_empty_bins(self, df):
         bins = np.arange(1, 11) / 10
-        ax = self.df.plot(
+        ax = df.plot(
             "low_vals",
             scheme="UserDefined",
             classification_kwds={"bins": bins},
@@ -1346,7 +1356,7 @@ class TestMapclassifyPlotting:
             line.get_markerfacecolor() for line in ax.get_legend().get_lines()
         ] == legend_colors_exp
 
-        ax2 = self.df.plot(
+        ax2 = df.plot(
             "mid_vals",
             scheme="UserDefined",
             classification_kwds={"bins": bins},
@@ -1384,7 +1394,7 @@ class TestMapclassifyPlotting:
             line.get_markerfacecolor() for line in ax2.get_legend().get_lines()
         ] == legend_colors_exp
 
-        ax3 = self.df.plot(
+        ax3 = df.plot(
             "high_vals",
             scheme="UserDefined",
             classification_kwds={"bins": bins},
@@ -1409,8 +1419,8 @@ class TestMapclassifyPlotting:
             line.get_markerfacecolor() for line in ax3.get_legend().get_lines()
         ] == legend_colors_exp
 
-    def test_equally_formatted_bins(self):
-        ax = self.nybb.plot(
+    def test_equally_formatted_bins(self, nybb):
+        ax = nybb.plot(
             "vals",
             scheme="quantiles",
             legend=True,
@@ -1425,7 +1435,7 @@ class TestMapclassifyPlotting:
         ]
         assert labels == expected
 
-        ax2 = self.nybb.plot(
+        ax2 = nybb.plot(
             "vals", scheme="quantiles", legend=True, legend_kwds={"fmt": "{:.3f}"}
         )
         labels = [t.get_text() for t in ax2.get_legend().get_texts()]
diff --git a/geopandas/tests/test_sindex.py b/geopandas/tests/test_sindex.py
index 016f9307..51fb5dc0 100644
--- a/geopandas/tests/test_sindex.py
+++ b/geopandas/tests/test_sindex.py
@@ -15,7 +15,7 @@ from shapely.geometry import (
 )
 
 import geopandas
-from geopandas import GeoDataFrame, GeoSeries, datasets, read_file
+from geopandas import GeoDataFrame, GeoSeries, read_file
 from geopandas import _compat as compat
 
 
@@ -202,12 +202,12 @@ class TestFrameSindex:
         assert old_sindex is new_sindex
 
 
-# Skip to accommodate Shapely geometries being unhashable
+# Skip to accommodate Shapely geometries being unhashable # TODO unskip?
 @pytest.mark.skip
+@pytest.mark.usefixtures("_setup_class_nybb_filename")
 class TestJoinSindex:
     def setup_method(self):
-        nybb_filename = geopandas.datasets.get_path("nybb")
-        self.boros = read_file(nybb_filename)
+        self.boros = read_file(self.nybb_filename)
 
     def test_merge_geo(self):
         # First check that we gets hits from the boros frame.
@@ -380,6 +380,108 @@ class TestShapelyInterface:
         with pytest.raises(TypeError):
             self.df.sindex.query("notavalidgeom")
 
+    @pytest.mark.skipif(not compat.GEOS_GE_310, reason="Requires GEOS 3.10")
+    @pytest.mark.parametrize(
+        "distance, test_geom, expected",
+        (
+            # bounds don't intersect and not within distance=0
+            (
+                0,
+                box(9.0, 9.0, 9.9, 9.9),
+                [],
+            ),
+            # bounds don't intersect but is within distance=1
+            (
+                1,
+                box(9.0, 9.0, 9.9, 9.9),
+                [5],
+            ),
+            # within 1-D absolute distance in both axes, but not euclidean distance
+            (
+                0.5,
+                Point(0.5, 0.5),
+                [],
+            ),
+            # same as before but within euclidean distance
+            (
+                sqrt(2 * 0.5**2) + 1e-9,
+                Point(0.5, 0.5),
+                [0, 1],
+            ),
+            # less than euclidean distance between points, multi-object
+            (
+                sqrt(2) - 1e-9,
+                [
+                    Polygon([(0, 0), (1, 0), (1, 1)]),
+                    Polygon([(1, 1), (2, 1), (2, 2)]),
+                ],  # multi-object test
+                [[0, 0, 1, 1], [0, 1, 1, 2]],
+            ),
+            # more than euclidean distance between points, multi-object
+            (
+                sqrt(2) + 1e-9,
+                [
+                    Polygon([(0, 0), (1, 0), (1, 1)]),
+                    Polygon([(1, 1), (2, 1), (2, 2)]),
+                ],
+                [[0, 0, 0, 1, 1, 1, 1], [0, 1, 2, 0, 1, 2, 3]],
+            ),
+            # distance is array-like, broadcastable to geometry
+            (
+                [2, 10],
+                [Point(0.5, 0.5), Point(1, 1)],
+                [[0, 0, 1, 1, 1, 1, 1], [0, 1, 0, 1, 2, 3, 4]],
+            ),
+        ),
+    )
+    def test_query_dwithin(self, distance, test_geom, expected):
+        """Tests the `query` method with predicates that require keyword arguments."""
+        res = self.df.sindex.query(test_geom, predicate="dwithin", distance=distance)
+        assert_array_equal(res, expected)
+
+    @pytest.mark.skipif(not compat.GEOS_GE_310, reason="Requires GEOS 3.10")
+    def test_dwithin_no_distance(self):
+        """Tests the `query` method with keyword arguments that are
+        invalid for certain predicates."""
+        with pytest.raises(
+            ValueError, match="'distance' parameter is required for 'dwithin' predicate"
+        ):
+            self.df.sindex.query(Point(0, 0), predicate="dwithin")
+
+    @pytest.mark.parametrize(
+        "predicate",
+        [
+            None,
+            "contains",
+            "contains_properly",
+            "covered_by",
+            "covers",
+            "crosses",
+            "intersects",
+            "overlaps",
+            "touches",
+            "within",
+        ],
+    )
+    def test_query_distance_invalid(self, predicate):
+        """Tests the `query` method with keyword arguments that are
+        invalid for certain predicates."""
+        msg = "'distance' parameter is only supported in combination with 'dwithin'"
+        with pytest.raises(ValueError, match=msg):
+            self.df.sindex.query(Point(0, 0), predicate=predicate, distance=0)
+
+    @pytest.mark.skipif(
+        compat.GEOS_GE_310, reason="Test for 'dwithin'-incompatible versions of GEOS"
+    )
+    def test_dwithin_requirements(self):
+        """Tests whether a ValueError is raised when trying to use dwithin with
+        incompatible versions of shapely or pyGEOS
+        """
+        with pytest.raises(
+            ValueError, match="predicate = 'dwithin' requires GEOS >= 3.10.0"
+        ):
+            self.df.sindex.query(Point(0, 0), predicate="dwithin", distance=0)
+
     @pytest.mark.parametrize(
         "test_geom, expected_value",
         [
@@ -845,10 +947,12 @@ class TestShapelyInterface:
             ("touches", (2, 0)),
         ],
     )
-    def test_integration_natural_earth(self, predicate, expected_shape):
+    def test_integration_natural_earth(
+        self, predicate, expected_shape, naturalearth_lowres, naturalearth_cities
+    ):
         """Tests output sizes for the naturalearth datasets."""
-        world = read_file(datasets.get_path("naturalearth_lowres"))
-        capitals = read_file(datasets.get_path("naturalearth_cities"))
+        world = read_file(naturalearth_lowres)
+        capitals = read_file(naturalearth_cities)
 
         res = world.sindex.query(capitals.geometry, predicate)
         assert res.shape == expected_shape
diff --git a/geopandas/tests/util.py b/geopandas/tests/util.py
index 9e7021ef..e21ab963 100644
--- a/geopandas/tests/util.py
+++ b/geopandas/tests/util.py
@@ -13,6 +13,15 @@ from geopandas.testing import (  # noqa: F401
 HERE = os.path.abspath(os.path.dirname(__file__))
 PACKAGE_DIR = os.path.dirname(os.path.dirname(HERE))
 
+_TEST_DATA_DIR = os.path.join(PACKAGE_DIR, "geopandas", "tests", "data")
+_NYBB = "zip://" + os.path.join(_TEST_DATA_DIR, "nybb_16a.zip")
+_NATURALEARTH_CITIES = os.path.join(
+    _TEST_DATA_DIR, "naturalearth_cities", "naturalearth_cities.shp"
+)
+_NATURALEARTH_LOWRES = os.path.join(
+    _TEST_DATA_DIR, "naturalearth_lowres", "naturalearth_lowres.shp"
+)
+
 
 # mock not used here, but the import from here is used in other modules
 try:
diff --git a/geopandas/tools/tests/test_random.py b/geopandas/tools/tests/test_random.py
index 5e09e2a7..5a810d82 100644
--- a/geopandas/tools/tests/test_random.py
+++ b/geopandas/tools/tests/test_random.py
@@ -4,18 +4,38 @@ import geopandas
 
 from geopandas.tools._random import uniform
 
-multipolygons = geopandas.read_file(geopandas.datasets.get_path("nybb")).geometry
-polygons = multipolygons.explode(ignore_index=True).geometry
-multilinestrings = multipolygons.boundary
-linestrings = polygons.boundary
-points = multipolygons.centroid
+
+@pytest.fixture
+def multipolygons(nybb_filename):
+    return geopandas.read_file(nybb_filename).geometry
+
+
+@pytest.fixture
+def polygons(multipolygons):
+    return multipolygons.explode(ignore_index=True).geometry
+
+
+@pytest.fixture
+def multilinestrings(multipolygons):
+    return multipolygons.boundary
+
+
+@pytest.fixture
+def linestrings(polygons):
+    return polygons.boundary
+
+
+@pytest.fixture
+def points(multipolygons):
+    return multipolygons.centroid
 
 
 @pytest.mark.parametrize("size", [10, 100])
 @pytest.mark.parametrize(
-    "geom", [multipolygons[0], polygons[0], multilinestrings[0], linestrings[0]]
+    "geom_fixture", ["multipolygons", "polygons", "multilinestrings", "linestrings"]
 )
-def test_uniform(geom, size):
+def test_uniform(geom_fixture, size, request):
+    geom = request.getfixturevalue(geom_fixture)[0]
     sample = uniform(geom, size=size, rng=1)
     sample_series = (
         geopandas.GeoSeries(sample).explode(index_parts=True).reset_index(drop=True)
@@ -27,13 +47,13 @@ def test_uniform(geom, size):
     assert len(sample_in_geom) == size
 
 
-def test_uniform_unsupported():
+def test_uniform_unsupported(points):
     with pytest.warns(UserWarning, match="Sampling is not supported"):
         sample = uniform(points[0], size=10, rng=1)
     assert sample.is_empty
 
 
-def test_uniform_generator():
+def test_uniform_generator(polygons):
     sample = uniform(polygons[0], size=10, rng=1)
     sample2 = uniform(polygons[0], size=10, rng=1)
     assert sample.equals(sample2)
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index 7b94fb55..bc156c72 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -362,11 +362,66 @@ class TestSpatialJoin:
 
         assert_frame_equal(res, exp, check_index_type=False)
 
+    @pytest.mark.parametrize("how", ["inner"])
+    @pytest.mark.parametrize(
+        "geo_left, geo_right, expected_left, expected_right, distance",
+        [
+            (
+                # Distance is number, 2x1
+                [Point(0, 0), Point(1, 1)],
+                [Point(1, 1)],
+                [0, 1],
+                [0, 0],
+                math.sqrt(2),
+            ),
+            # Distance is number, 2x2
+            (
+                [Point(0, 0), Point(1, 1)],
+                [Point(0, 0), Point(1, 1)],
+                [0, 1, 0, 1],
+                [0, 0, 1, 1],
+                math.sqrt(2),
+            ),
+            # Distance is array, matches len(left)
+            (
+                [Point(0, 0), Point(0, 0), Point(-1, -1)],
+                [Point(1, 1)],
+                [1, 2],
+                [0, 0],
+                [0, math.sqrt(2), math.sqrt(8)],
+            ),
+            # Distance is np.array, matches len(left),
+            # inner join sorts the right GeoDataFrame
+            (
+                [Point(0, 0), Point(0, 0), Point(-1, -1)],
+                [Point(1, 1), Point(0.5, 0.5)],
+                [1, 2, 1, 2],
+                [1, 1, 0, 0],
+                np.array([0, math.sqrt(2), math.sqrt(8)]),
+            ),
+        ],
+    )
+    def test_sjoin_dwithin(
+        self,
+        geo_left,
+        geo_right,
+        expected_left: Sequence[int],
+        expected_right: Sequence[int],
+        distance,
+        how,
+    ):
+        left = geopandas.GeoDataFrame({"geometry": geo_left})
+        right = geopandas.GeoDataFrame({"geometry": geo_right})
+        expected_gdf = left.iloc[expected_left].copy()
+        expected_gdf["index_right"] = expected_right
+        joined = sjoin(left, right, how=how, predicate="dwithin", distance=distance)
+        assert_frame_equal(expected_gdf.sort_index(), joined.sort_index())
+
 
+@pytest.mark.usefixtures("_setup_class_nybb_filename")
 class TestSpatialJoinNYBB:
     def setup_method(self):
-        nybb_filename = geopandas.datasets.get_path("nybb")
-        self.polydf = read_file(nybb_filename)
+        self.polydf = read_file(self.nybb_filename)
         self.crs = self.polydf.crs
         N = 20
         b = [int(x) for x in self.polydf.total_bounds]
@@ -536,8 +591,8 @@ class TestSpatialJoinNYBB:
         assert sjoin(empty, self.pointdf, how="inner", predicate=predicate).empty
         assert sjoin(empty, self.pointdf, how="left", predicate=predicate).empty
 
-    def test_empty_sjoin_return_duplicated_columns(self):
-        nybb = geopandas.read_file(geopandas.datasets.get_path("nybb"))
+    def test_empty_sjoin_return_duplicated_columns(self, nybb_filename):
+        nybb = geopandas.read_file(nybb_filename)
         nybb2 = nybb.copy()
         nybb2.geometry = nybb2.translate(200000)  # to get non-overlapping
 
@@ -547,21 +602,22 @@ class TestSpatialJoinNYBB:
         assert "BoroCode_left" in result.columns
 
 
-class TestSpatialJoinNaturalEarth:
-    def setup_method(self):
-        world_path = geopandas.datasets.get_path("naturalearth_lowres")
-        cities_path = geopandas.datasets.get_path("naturalearth_cities")
-        self.world = read_file(world_path)
-        self.cities = read_file(cities_path)
+@pytest.fixture
+def world(naturalearth_lowres):
+    return read_file(naturalearth_lowres)
 
-    def test_sjoin_inner(self):
-        # GH637
-        countries = self.world[["geometry", "name"]]
-        countries = countries.rename(columns={"name": "country"})
-        cities_with_country = sjoin(
-            self.cities, countries, how="inner", predicate="intersects"
-        )
-        assert cities_with_country.shape == (213, 4)
+
+@pytest.fixture
+def cities(naturalearth_cities):
+    return read_file(naturalearth_cities)
+
+
+def test_sjoin_inner(world, cities):
+    # GH637
+    countries = world[["geometry", "name"]]
+    countries = countries.rename(columns={"name": "country"})
+    cities_with_country = sjoin(cities, countries, how="inner", predicate="intersects")
+    assert cities_with_country.shape == (213, 4)
 
 
 class TestNearest:
@@ -872,10 +928,10 @@ class TestNearest:
         assert_geodataframe_equal(expected_gdf, joined)
 
     @pytest.mark.filterwarnings("ignore:Geometry is in a geographic CRS")
-    def test_sjoin_nearest_inner(self):
+    def test_sjoin_nearest_inner(self, naturalearth_lowres, naturalearth_cities):
         # check equivalency of left and inner join
-        countries = read_file(geopandas.datasets.get_path("naturalearth_lowres"))
-        cities = read_file(geopandas.datasets.get_path("naturalearth_cities"))
+        countries = read_file(naturalearth_lowres)
+        cities = read_file(naturalearth_cities)
         countries = countries[["geometry", "name"]].rename(columns={"name": "country"})
 
         # default: inner and left give the same result