Repository: geopandas/geopandas
Pull request #3545 (2025-04-09 19:30:52)

ENH: add is_valid_coverage and invalid_coverage_edges
xref #2010

following the discussion we had in the dev call and #3541 , this uses natural language order rather than shapely's `coverage_` prefix.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a9db5f9..c09151a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ New features and improvements:
 - Added `disjoint_subset` union algorithm for `union_all` and `dissolve` (#3534).
 - Added `to_pandas_kwargs` argument to `from_arrow`, `read_parquet` and `read_feather`
   to allow better control of conversion of non-geometric Arrow data to DataFrames (#3466).
+- Added `is_valid_coverage` and `invalid_coverage_edges` to GeoSeries/GeoDataFrame to allow validation of polygonal coverage (#3545).
 - Added ``method`` and ``keep_collapsed`` argument to ``make_valid`` (#3548).
 
 Bug fixes:
@@ -78,30 +79,30 @@ Notes on dependencies:
 
 New methods:
 
-- Added `count_geometries` method from shapely to GeoSeries/GeoDataframe (#3154).
-- Added `count_interior_rings` method from shapely to GeoSeries/GeoDataframe (#3154)
-- Added `relate_pattern` method from shapely to GeoSeries/GeoDataframe (#3211).
-- Added `intersection_all` method from shapely to GeoSeries/GeoDataframe (#3228).
-- Added `line_merge` method from shapely to GeoSeries/GeoDataframe (#3214).
-- Added `set_precision` and `get_precision` methods from shapely to GeoSeries/GeoDataframe (#3175).
-- Added `count_coordinates` method from shapely to GeoSeries/GeoDataframe (#3026).
-- Added `minimum_clearance` method from shapely to GeoSeries/GeoDataframe (#2989).
-- Added `shared_paths` method from shapely to GeoSeries/GeoDataframe (#3215).
-- Added `is_ccw` method from shapely to GeoSeries/GeoDataframe (#3027).
-- Added `is_closed` attribute from shapely to GeoSeries/GeoDataframe (#3092).
-- Added `force_2d` and `force_3d` methods from shapely to GeoSeries/GeoDataframe (#3090).
-- Added `voronoi_polygons` method from shapely to GeoSeries/GeoDataframe (#3177).
-- Added `contains_properly` method from shapely to GeoSeries/GeoDataframe (#3105).
-- Added `build_area` method exposing `build_area` shapely to GeoSeries/GeoDataframe (#3202).
-- Added `snap` method from shapely to GeoSeries/GeoDataframe (#3086).
+- Added `count_geometries` method from shapely to GeoSeries/GeoDataFrame (#3154).
+- Added `count_interior_rings` method from shapely to GeoSeries/GeoDataFrame (#3154)
+- Added `relate_pattern` method from shapely to GeoSeries/GeoDataFrame (#3211).
+- Added `intersection_all` method from shapely to GeoSeries/GeoDataFrame (#3228).
+- Added `line_merge` method from shapely to GeoSeries/GeoDataFrame (#3214).
+- Added `set_precision` and `get_precision` methods from shapely to GeoSeries/GeoDataFrame (#3175).
+- Added `count_coordinates` method from shapely to GeoSeries/GeoDataFrame (#3026).
+- Added `minimum_clearance` method from shapely to GeoSeries/GeoDataFrame (#2989).
+- Added `shared_paths` method from shapely to GeoSeries/GeoDataFrame (#3215).
+- Added `is_ccw` method from shapely to GeoSeries/GeoDataFrame (#3027).
+- Added `is_closed` attribute from shapely to GeoSeries/GeoDataFrame (#3092).
+- Added `force_2d` and `force_3d` methods from shapely to GeoSeries/GeoDataFrame (#3090).
+- Added `voronoi_polygons` method from shapely to GeoSeries/GeoDataFrame (#3177).
+- Added `contains_properly` method from shapely to GeoSeries/GeoDataFrame (#3105).
+- Added `build_area` method exposing `build_area` shapely to GeoSeries/GeoDataFrame (#3202).
+- Added `snap` method from shapely to GeoSeries/GeoDataFrame (#3086).
 - Added `transform` method from shapely to GeoSeries/GeoDataFrame (#3075).
-- Added `get_geometry` method from shapely to GeoSeries/GeoDataframe (#3287).
+- Added `get_geometry` method from shapely to GeoSeries/GeoDataFrame (#3287).
 - Added `dwithin` method to check for a "distance within" predicate on
   GeoSeries/GeoDataFrame (#3153).
 - Added `to_geo_dict` method to generate GeoJSON-like dictionary from a GeoDataFrame (#3132).
 - Added `polygonize` method exposing both `polygonize` and `polygonize_full` from
-  shapely to GeoSeries/GeoDataframe (#2963).
-- Added `is_valid_reason` method from shapely to GeoSeries/GeoDataframe (#3176).
+  shapely to GeoSeries/GeoDataFrame (#2963).
+- Added `is_valid_reason` method from shapely to GeoSeries/GeoDataFrame (#3176).
 - Added `to_arrow` method and `from_arrow` class method to
   GeoSeries/GeoDataFrame to export and import to/from Arrow data with GeoArrow
   extension types (#3219, #3301).
@@ -161,7 +162,7 @@ Backwards incompatible API changes:
   the previous active geometry column name. This means that if the new and old names are
   different, then both columns will be preserved in the GeoDataFrame. To replicate the previous
   behaviour, you can instead call `gdf.set_geometry(ser.rename(gdf.active_geometry_name))` (#3237).
-  Note that this behaviour change does not affect the `GeoDataframe` constructor, passing a named
+  Note that this behaviour change does not affect the `GeoDataFrame` constructor, passing a named
   GeoSeries `ser` to `GeoDataFrame(df, geometry=ser)` will always produce a GeoDataFrame with a
   geometry column named "geometry" to preserve backwards compatibility. If you would like to
   instead propagate the name of `ser` when constructing a GeoDataFrame, you can instead call
@@ -273,17 +274,17 @@ API changes:
 
 New methods:
 
-- Added ``concave_hull`` method from shapely to GeoSeries/GeoDataframe (#2903).
-- Added ``delaunay_triangles`` method from shapely to GeoSeries/GeoDataframe (#2907).
-- Added ``extract_unique_points`` method from shapely to GeoSeries/GeoDataframe (#2915).
-- Added ``frechet_distance()`` method from shapely to GeoSeries/GeoDataframe (#2929).
-- Added ``hausdorff_distance`` method from shapely to GeoSeries/GeoDataframe (#2909).
-- Added ``minimum_rotated_rectangle`` method from shapely to GeoSeries/GeoDataframe (#2541).
-- Added ``offset_curve`` method from shapely to GeoSeries/GeoDataframe (#2902).
-- Added ``remove_repeated_points`` method from shapely to GeoSeries/GeoDataframe (#2940).
-- Added ``reverse`` method from shapely to GeoSeries/GeoDataframe (#2988).
+- Added ``concave_hull`` method from shapely to GeoSeries/GeoDataFrame (#2903).
+- Added ``delaunay_triangles`` method from shapely to GeoSeries/GeoDataFrame (#2907).
+- Added ``extract_unique_points`` method from shapely to GeoSeries/GeoDataFrame (#2915).
+- Added ``frechet_distance()`` method from shapely to GeoSeries/GeoDataFrame (#2929).
+- Added ``hausdorff_distance`` method from shapely to GeoSeries/GeoDataFrame (#2909).
+- Added ``minimum_rotated_rectangle`` method from shapely to GeoSeries/GeoDataFrame (#2541).
+- Added ``offset_curve`` method from shapely to GeoSeries/GeoDataFrame (#2902).
+- Added ``remove_repeated_points`` method from shapely to GeoSeries/GeoDataFrame (#2940).
+- Added ``reverse`` method from shapely to GeoSeries/GeoDataFrame (#2988).
 - Added ``segmentize`` method from shapely to GeoSeries/GeoDataFrame (#2910).
-- Added ``shortest_line`` method from shapely to GeoSeries/GeoDataframe (#2960).
+- Added ``shortest_line`` method from shapely to GeoSeries/GeoDataFrame (#2960).
 
 New features and improvements:
 
@@ -339,8 +340,8 @@ New methods:
   for each geometry in a GeoSeries/GeoDataFrame (#2297).
 - Support for sorting geometries (for example, using ``sort_values()``) based on
   the distance along the Hilbert curve (#2070).
-- Added ``get_coordinates()`` method from shapely to GeoSeries/GeoDataframe (#2624).
-- Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataframe (#2621).
+- Added ``get_coordinates()`` method from shapely to GeoSeries/GeoDataFrame (#2624).
+- Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataFrame (#2621).
 - Added `minimum_bounding_radius()` as GeoSeries method (#2827).
 
 Other new features and improvements:
@@ -413,8 +414,8 @@ for more details.
 
 New features and improvements:
 
-- Added ``normalize()`` method from shapely to GeoSeries/GeoDataframe (#2537).
-- Added ``make_valid()`` method from shapely to GeoSeries/GeoDataframe (#2539).
+- Added ``normalize()`` method from shapely to GeoSeries/GeoDataFrame (#2537).
+- Added ``make_valid()`` method from shapely to GeoSeries/GeoDataFrame (#2539).
 - Added ``where`` filter to ``read_file`` (#2552).
 - Updated the distributed natural earth datasets (*naturalearth_lowres* and
   *naturalearth_cities*) to version 5.1 (#2555).
@@ -875,7 +876,7 @@ New features and improvements:
     legend: ``fmt`` with a format string for the bin edges (#1253), and ``labels``
     to pass fully custom class labels (#1302).
 
-- New ``covers()`` and ``covered_by()`` methods on GeoSeries/GeoDataframe for the
+- New ``covers()`` and ``covered_by()`` methods on GeoSeries/GeoDataFrame for the
   equivalent spatial predicates (#1460, #1462).
 - GeoPandas now warns when using distance-based methods with data in a
   geographic projection (#1378).
diff --git a/doc/source/about/citing.md b/doc/source/about/citing.md
index e01ef478..00236715 100644
--- a/doc/source/about/citing.md
+++ b/doc/source/about/citing.md
@@ -37,7 +37,7 @@ _Kelsey Jordahl, Joris Van den Bossche, Martin Fleischmann, Jacob Wasserman, Jam
                   Adam Greenhall and
                   Chris Holdgraf and
                   Filipe and
-                  François Leblanc},
+                  Fran\c{c}ois Leblanc},
   title        = {geopandas/geopandas: v0.8.1},
   month        = jul,
   year         = 2020,
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 2f55e987..2a3271b4 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -172,11 +172,6 @@ html_theme_options = {
             "url": "https://github.com/geopandas/geopandas",
             "icon": "fab fa-github-square fa-xl",
         },
-        {
-            "name": "Twitter",
-            "url": "https://twitter.com/geopandas",
-            "icon": "fab fa-twitter-square fa-xl",
-        },
     ]
 }
 
diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst
index c5ce1a7e..ec0104b8 100644
--- a/doc/source/docs/reference/geoseries.rst
+++ b/doc/source/docs/reference/geoseries.rst
@@ -53,6 +53,8 @@ Unary predicates
    GeoSeries.is_simple
    GeoSeries.is_valid
    GeoSeries.is_valid_reason
+   GeoSeries.is_valid_coverage
+   GeoSeries.invalid_coverage_edges
    GeoSeries.has_z
    GeoSeries.is_ccw
 
diff --git a/doc/source/docs/user_guide/pygeos_to_shapely.rst b/doc/source/docs/user_guide/pygeos_to_shapely.rst
index c63f22a6..470ee2a3 100644
--- a/doc/source/docs/user_guide/pygeos_to_shapely.rst
+++ b/doc/source/docs/user_guide/pygeos_to_shapely.rst
@@ -31,7 +31,7 @@ GeoPandas 0.14
   if Shapely 1.8 is installed instead of 2.0 or newer. The PyGEOS backend is still
   supported, but a user needs to opt in using the environment variable
   ``USE_PYGEOS`` as explained in the
-  `installation instructions <../getting_started/install>`__.
+  `installation instructions <../../getting_started/install.rst>`__.
 
 GeoPandas 1.0
 ^^^^^^^^^^^^^
@@ -75,4 +75,4 @@ together with a change of ``gdf.geometry.values.data`` to ``gdf.geometry.values`
 analogous ``gdf.geometry.array``  should work, there are some differences between the
 API of PyGEOS and that of Shapely. Please consult the
 `Migrating from PyGEOS <https://shapely.readthedocs.io/en/stable/migration_pygeos.html>`__
-document for details.
\ No newline at end of file
+document for details.
diff --git a/doc/source/gallery/plot_clip.ipynb b/doc/source/gallery/plot_clip.ipynb
index 707164dd..023bb2c6 100644
--- a/doc/source/gallery/plot_clip.ipynb
+++ b/doc/source/gallery/plot_clip.ipynb
@@ -1,236 +1,236 @@
 {
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "# Clip Vector Data with GeoPandas\n",
-    "\n",
-    "\n",
-    "Learn how to clip geometries to the boundary of a polygon geometry\n",
-    "using GeoPandas."
-   ]
-  },
-  {
-   "attachments": {},
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "The example below shows you how to clip a set of vector geometries\n",
-    "to the spatial extent / shape of another vector object. Both sets of geometries\n",
-    "must be opened with GeoPandas as GeoDataFrames and be in the same Coordinate\n",
-    "Reference System (CRS) for the `clip` function in GeoPandas to work.\n",
-    "\n",
-    "This example uses example data from ``geodatasets`` ``'geoda.chicago-health'`` and\n",
-    "``'geoda.groceries'``, alongside a custom rectangle geometry made with\n",
-    "shapely and then turned into a GeoDataFrame.\n",
-    "\n",
-    "<div class=\"alert alert-info\">\n",
-    "    \n",
-    "Note\n",
-    "\n",
-    "The object to be clipped will be clipped to the full extent of the clip\n",
-    "object. If there are multiple polygons in clip object, the input data will\n",
-    "be clipped to the total boundary of all polygons in clip object.\n",
-    "</div>\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Import Packages\n",
-    "---------------\n",
-    "\n",
-    "To begin, import the needed packages.\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import matplotlib.pyplot as plt\n",
-    "import geopandas\n",
-    "from shapely.geometry import box\n",
-    "import geodatasets"
-   ]
-  },
-  {
-   "attachments": {},
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Get or Create Example Data\n",
-    "--------------------------\n",
-    "\n",
-    "Below, the example GeoPandas data is imported and opened as a GeoDataFrame.\n",
-    "Additionally, a polygon is created with shapely and then converted into a\n",
-    "GeoDataFrame with the same CRS as the GeoPandas chicago dataset.\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "chicago = geopandas.read_file(geodatasets.get_path(\"geoda.chicago_commpop\"))\n",
-    "groceries = geopandas.read_file(geodatasets.get_path(\"geoda.groceries\")).to_crs(chicago.crs)\n",
-    "\n",
-    "# Create a subset of the chicago data that is just the South American continent\n",
-    "near_west_side = chicago[chicago[\"community\"] == \"NEAR WEST SIDE\"]\n",
-    "\n",
-    "# Create a custom polygon\n",
-    "polygon = box(-87.8, 41.90, -87.5, 42)\n",
-    "poly_gdf = geopandas.GeoDataFrame([1], geometry=[polygon], crs=chicago.crs)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Plot the Unclipped Data\n",
-    "-----------------------\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))\n",
-    "chicago.plot(ax=ax1)\n",
-    "poly_gdf.boundary.plot(ax=ax1, color=\"red\")\n",
-    "near_west_side.boundary.plot(ax=ax2, color=\"green\")\n",
-    "groceries.plot(ax=ax2, color=\"purple\")\n",
-    "ax1.set_title(\"All Unclipped Chicago Communities\", fontsize=20)\n",
-    "ax2.set_title(\"All Unclipped Groceries\", fontsize=20)\n",
-    "ax1.set_axis_off()\n",
-    "ax2.set_axis_off()\n",
-    "plt.show()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Clip the Data\n",
-    "--------------\n",
-    "\n",
-    "The object on which you call `clip` is the object that will\n",
-    "be clipped. The object you pass is the clip extent. The returned output\n",
-    "will be a new clipped GeoDataframe. All of the attributes for each returned\n",
-    "geometry will be retained when you clip.\n",
-    "\n",
-    "<div class=\"alert alert-info\">\n",
-    "\n",
-    "Note\n",
-    "\n",
-    "Recall that the data must be in the same CRS in order to use the\n",
-    "`clip` method. If the data are not in the same CRS, be sure to use\n",
-    "the GeoPandas `GeoDataFrame.to_crs` method to ensure both datasets\n",
-    "are in the same CRS.\n",
-    "</div>\n",
-    "\n"
-   ]
-  },
-  {
-   "attachments": {},
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Clip the chicago Data\n",
-    "--------------------\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "tags": [
-     "nbsphinx-thumbnail"
-    ]
-   },
-   "outputs": [],
-   "source": [
-    "chicago_clipped = chicago.clip(polygon)\n",
-    "\n",
-    "# Plot the clipped data\n",
-    "# The plot below shows the results of the clip function applied to the chicago\n",
-    "# sphinx_gallery_thumbnail_number = 2\n",
-    "fig, ax = plt.subplots(figsize=(12, 8))\n",
-    "chicago_clipped.plot(ax=ax, color=\"purple\")\n",
-    "chicago.boundary.plot(ax=ax)\n",
-    "poly_gdf.boundary.plot(ax=ax, color=\"red\")\n",
-    "ax.set_title(\"Chicago Clipped\", fontsize=20)\n",
-    "ax.set_axis_off()\n",
-    "plt.show()"
-   ]
-  },
-  {
-   "attachments": {},
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "<div class=\"alert alert-info\">\n",
-    "    \n",
-    "Note\n",
-    "\n",
-    "For historical reasons, the clip method is also available as a top-level function `geopandas.clip`.\n",
-    "It is recommended to use the method as the function may be deprecated in the future.\n",
-    "</div>\n",
-    "\n",
-    "Clip the groceries Data\n",
-    "----------------------\n",
-    "\n"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "groceries_clipped = groceries.clip(near_west_side)\n",
-    "\n",
-    "# Plot the clipped data\n",
-    "# The plot below shows the results of the clip function applied to the capital cities\n",
-    "fig, ax = plt.subplots(figsize=(12, 8))\n",
-    "groceries_clipped.plot(ax=ax, color=\"purple\")\n",
-    "near_west_side.boundary.plot(ax=ax, color=\"green\")\n",
-    "ax.set_title(\"Groceries Clipped\", fontsize=20)\n",
-    "ax.set_axis_off()\n",
-    "plt.show()"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.10.8"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 4
-}
+    "cells": [
+        {
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "# Clip Vector Data with GeoPandas\n",
+                "\n",
+                "\n",
+                "Learn how to clip geometries to the boundary of a polygon geometry\n",
+                "using GeoPandas."
+            ]
+        },
+        {
+            "attachments": {},
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "The example below shows you how to clip a set of vector geometries\n",
+                "to the spatial extent / shape of another vector object. Both sets of geometries\n",
+                "must be opened with GeoPandas as GeoDataFrames and be in the same Coordinate\n",
+                "Reference System (CRS) for the `clip` function in GeoPandas to work.\n",
+                "\n",
+                "This example uses example data from ``geodatasets`` ``'geoda.chicago-health'`` and\n",
+                "``'geoda.groceries'``, alongside a custom rectangle geometry made with\n",
+                "shapely and then turned into a GeoDataFrame.\n",
+                "\n",
+                "<div class=\"alert alert-info\">\n",
+                "    \n",
+                "Note\n",
+                "\n",
+                "The object to be clipped will be clipped to the full extent of the clip\n",
+                "object. If there are multiple polygons in clip object, the input data will\n",
+                "be clipped to the total boundary of all polygons in clip object.\n",
+                "</div>\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "Import Packages\n",
+                "---------------\n",
+                "\n",
+                "To begin, import the needed packages.\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "code",
+            "execution_count": null,
+            "metadata": {},
+            "outputs": [],
+            "source": [
+                "import matplotlib.pyplot as plt\n",
+                "import geopandas\n",
+                "from shapely.geometry import box\n",
+                "import geodatasets"
+            ]
+        },
+        {
+            "attachments": {},
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "Get or Create Example Data\n",
+                "--------------------------\n",
+                "\n",
+                "Below, the example GeoPandas data is imported and opened as a GeoDataFrame.\n",
+                "Additionally, a polygon is created with shapely and then converted into a\n",
+                "GeoDataFrame with the same CRS as the GeoPandas chicago dataset.\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "code",
+            "execution_count": null,
+            "metadata": {},
+            "outputs": [],
+            "source": [
+                "chicago = geopandas.read_file(geodatasets.get_path(\"geoda.chicago_commpop\"))\n",
+                "groceries = geopandas.read_file(geodatasets.get_path(\"geoda.groceries\")).to_crs(chicago.crs)\n",
+                "\n",
+                "# Create a subset of the chicago data that is just the South American continent\n",
+                "near_west_side = chicago[chicago[\"community\"] == \"NEAR WEST SIDE\"]\n",
+                "\n",
+                "# Create a custom polygon\n",
+                "polygon = box(-87.8, 41.90, -87.5, 42)\n",
+                "poly_gdf = geopandas.GeoDataFrame([1], geometry=[polygon], crs=chicago.crs)"
+            ]
+        },
+        {
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "Plot the Unclipped Data\n",
+                "-----------------------\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "code",
+            "execution_count": null,
+            "metadata": {},
+            "outputs": [],
+            "source": [
+                "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))\n",
+                "chicago.plot(ax=ax1)\n",
+                "poly_gdf.boundary.plot(ax=ax1, color=\"red\")\n",
+                "near_west_side.boundary.plot(ax=ax2, color=\"green\")\n",
+                "groceries.plot(ax=ax2, color=\"purple\")\n",
+                "ax1.set_title(\"All Unclipped Chicago Communities\", fontsize=20)\n",
+                "ax2.set_title(\"All Unclipped Groceries\", fontsize=20)\n",
+                "ax1.set_axis_off()\n",
+                "ax2.set_axis_off()\n",
+                "plt.show()"
+            ]
+        },
+        {
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "Clip the Data\n",
+                "--------------\n",
+                "\n",
+                "The object on which you call `clip` is the object that will\n",
+                "be clipped. The object you pass is the clip extent. The returned output\n",
+                "will be a new clipped GeoDataFrame. All of the attributes for each returned\n",
+                "geometry will be retained when you clip.\n",
+                "\n",
+                "<div class=\"alert alert-info\">\n",
+                "\n",
+                "Note\n",
+                "\n",
+                "Recall that the data must be in the same CRS in order to use the\n",
+                "`clip` method. If the data are not in the same CRS, be sure to use\n",
+                "the GeoPandas `GeoDataFrame.to_crs` method to ensure both datasets\n",
+                "are in the same CRS.\n",
+                "</div>\n",
+                "\n"
+            ]
+        },
+        {
+            "attachments": {},
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "Clip the chicago Data\n",
+                "--------------------\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "code",
+            "execution_count": null,
+            "metadata": {
+                "tags": [
+                    "nbsphinx-thumbnail"
+                ]
+            },
+            "outputs": [],
+            "source": [
+                "chicago_clipped = chicago.clip(polygon)\n",
+                "\n",
+                "# Plot the clipped data\n",
+                "# The plot below shows the results of the clip function applied to the chicago\n",
+                "# sphinx_gallery_thumbnail_number = 2\n",
+                "fig, ax = plt.subplots(figsize=(12, 8))\n",
+                "chicago_clipped.plot(ax=ax, color=\"purple\")\n",
+                "chicago.boundary.plot(ax=ax)\n",
+                "poly_gdf.boundary.plot(ax=ax, color=\"red\")\n",
+                "ax.set_title(\"Chicago Clipped\", fontsize=20)\n",
+                "ax.set_axis_off()\n",
+                "plt.show()"
+            ]
+        },
+        {
+            "attachments": {},
+            "cell_type": "markdown",
+            "metadata": {},
+            "source": [
+                "<div class=\"alert alert-info\">\n",
+                "    \n",
+                "Note\n",
+                "\n",
+                "For historical reasons, the clip method is also available as a top-level function `geopandas.clip`.\n",
+                "It is recommended to use the method as the function may be deprecated in the future.\n",
+                "</div>\n",
+                "\n",
+                "Clip the groceries Data\n",
+                "----------------------\n",
+                "\n"
+            ]
+        },
+        {
+            "cell_type": "code",
+            "execution_count": null,
+            "metadata": {},
+            "outputs": [],
+            "source": [
+                "groceries_clipped = groceries.clip(near_west_side)\n",
+                "\n",
+                "# Plot the clipped data\n",
+                "# The plot below shows the results of the clip function applied to the capital cities\n",
+                "fig, ax = plt.subplots(figsize=(12, 8))\n",
+                "groceries_clipped.plot(ax=ax, color=\"purple\")\n",
+                "near_west_side.boundary.plot(ax=ax, color=\"green\")\n",
+                "ax.set_title(\"Groceries Clipped\", fontsize=20)\n",
+                "ax.set_axis_off()\n",
+                "plt.show()"
+            ]
+        }
+    ],
+    "metadata": {
+        "kernelspec": {
+            "display_name": "Python 3",
+            "language": "python",
+            "name": "python3"
+        },
+        "language_info": {
+            "codemirror_mode": {
+                "name": "ipython",
+                "version": 3
+            },
+            "file_extension": ".py",
+            "mimetype": "text/x-python",
+            "name": "python",
+            "nbconvert_exporter": "python",
+            "pygments_lexer": "ipython3",
+            "version": "3.10.8"
+        }
+    },
+    "nbformat": 4,
+    "nbformat_minor": 4
+}
\ No newline at end of file
diff --git a/geopandas/array.py b/geopandas/array.py
index e6aa474c..2ea65c5a 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -513,6 +513,20 @@ class GeometryArray(ExtensionArray):
     def is_valid_reason(self):
         return shapely.is_valid_reason(self._data)
 
+    def is_valid_coverage(self, gap_width=0.0):
+        if not (SHAPELY_GE_21 and GEOS_GE_312):
+            raise ImportError(
+                "Method 'is_valid_coverage' requires shapely>=2.1 and GEOS>=3.12."
+            )
+        return bool(shapely.coverage_is_valid(self._data, gap_width=gap_width))
+
+    def invalid_coverage_edges(self, gap_width=0.0):
+        if not (SHAPELY_GE_21 and GEOS_GE_312):
+            raise ImportError(
+                "Method 'invalid_coverage_edges' requires shapely>=2.1 and GEOS>=3.12."
+            )
+        return shapely.coverage_invalid_edges(self._data, gap_width=gap_width)
+
     @property
     def is_empty(self):
         return shapely.is_empty(self._data)
diff --git a/geopandas/base.py b/geopandas/base.py
index 6e1efb1f..4a816099 100644
--- a/geopandas/base.py
+++ b/geopandas/base.py
@@ -384,6 +384,132 @@ GeometryCollection
         """
         return Series(self.geometry.values.is_valid_reason(), index=self.index)
 
+    def is_valid_coverage(self, gap_width=0.0):
+        """Returns a ``bool`` indicating whether a ``GeoSeries`` forms a valid coverage
+
+        A ``GeoSeries`` of valid polygons is considered a coverage if the polygons are:
+
+        * **Non-overlapping** - polygons do not overlap (their interiors do not
+          intersect)
+        * **Edge-Matched** - vertices along shared edges are identical
+
+        A valid coverage may contain holes (regions of no coverage). However, sometimes
+        it might be desirable to detect narrow gaps as invalidities in the coverage. The
+        ``gap_width`` parameter allows to specify the maximum width of gaps to detect.
+        When gaps are detected, this method will return ``False`` and the
+        :meth:`coverage_invalid_edges` method can be used to find the edges of those
+        gaps.
+
+        Geometries that are not Polygon or MultiPolygon are ignored and an empty
+        LineString is returned.
+
+        Parameters
+        ----------
+        gap_width : float, optional
+            The maximum width of gaps to detect, by default 0.0
+
+        Returns
+        -------
+        bool
+
+        Examples
+        --------
+        >>> from shapely import Polygon
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+        ...         Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
+        ...     ]
+        ... )
+        >>> s
+        0    POLYGON ((0 0, 1 1, 1 0, 0 0))
+        1    POLYGON ((0 0, 1 1, 0 1, 0 0))
+        dtype: geometry
+
+        >>> s.is_valid_coverage()
+        True
+
+        Violation of edge-matching:
+
+        >>> s2 = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+        ...         Polygon([(0, 0), (0.5, 0.5), (1, 1), (0, 1), (0, 0)])
+        ...     ]
+        ... )
+        >>> s2
+        0             POLYGON ((0 0, 1 1, 1 0, 0 0))
+        1    POLYGON ((0 0, 0.5 0.5, 1 1, 0 1, 0 0))
+        dtype: geometry
+
+        >>> s2.is_valid_coverage()
+        False
+
+        See also
+        --------
+        GeoSeries.invalid_coverage_edges
+        GeoSeries.simplify_coverage
+        """
+        return self.geometry.values.is_valid_coverage(gap_width=gap_width)
+
+    def invalid_coverage_edges(self, gap_width=0.0):
+        """Returns a ``GeoSeries`` containing edges causing invalid polygonal coverage
+
+        This method returns (Multi)LineStrings showing the location of edges violating
+        polygonal coverage (if any) in each polygon in the input ``GeoSeries``.
+
+        A ``GeoSeries`` of valid polygons is considered a coverage if the polygons are:
+
+        * **Non-overlapping** - polygons do not overlap (their interiors do not
+          intersect)
+        * **Edge-Matched** - vertices along shared edges are identical
+
+        A valid coverage may contain holes (regions of no coverage). However, sometimes
+        it might be desirable to detect narrow gaps as invalidities in the coverage. The
+        ``gap_width`` parameter allows to specify the maximum width of gaps to detect.
+        When gaps are detected, the :meth:`is_valid_coverage` method will return
+        ``False`` and this method can be used to find the edges of those gaps.
+
+        Geometries that are not Polygon or MultiPolygon are ignored.
+
+        Parameters
+        ----------
+        gap_width : float, optional
+            The maximum width of gaps to detect, by default 0.0
+
+        Returns
+        -------
+        GeoSeries
+
+        Examples
+        --------
+        Violation of edge-matching:
+
+        >>> from shapely import Polygon
+        >>> s = geopandas.GeoSeries(
+        ...     [
+        ...         Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+        ...         Polygon([(0, 0), (0.5, 0.5), (1, 1), (0, 1), (0, 0)])
+        ...     ]
+        ... )
+        >>> s
+        0             POLYGON ((0 0, 1 1, 1 0, 0 0))
+        1    POLYGON ((0 0, 0.5 0.5, 1 1, 0 1, 0 0))
+        dtype: geometry
+
+        >>> s.invalid_coverage_edges()
+        0             LINESTRING (0 0, 1 1)
+        1    LINESTRING (0 0, 0.5 0.5, 1 1)
+        dtype: geometry
+
+
+        See also
+        --------
+        GeoSeries.is_valid_coverage
+        GeoSeries.simplify_coverage
+        """
+        return _delegate_geo_method("invalid_coverage_edges", self, gap_width=gap_width)
+
     @property
     def is_empty(self):
         """
diff --git a/geopandas/geodataframe.py b/geopandas/geodataframe.py
index 041d4643..7bfe6398 100644
--- a/geopandas/geodataframe.py
+++ b/geopandas/geodataframe.py
@@ -1957,7 +1957,7 @@ default 'snappy'
                 value = [value] * self.shape[0]
 
             crs = getattr(self, "crs", None)
-            # if we don't have a GeoDataframe yet and there is a column named crs,
+            # if we don't have a GeoDataFrame yet and there is a column named crs,
             # don't try to use that as a crs
             if isinstance(crs, pd.Series | pd.DataFrame):
                 crs = None


Test patch:
diff --git a/geopandas/tests/test_explore.py b/geopandas/tests/test_explore.py
index 03cf5e52..fc9bd659 100644
--- a/geopandas/tests/test_explore.py
+++ b/geopandas/tests/test_explore.py
@@ -386,7 +386,7 @@ class TestExplore:
             in out2_fields_str
         )
 
-        # GeoDataframe and the given list have different number of rows
+        # GeoDataFrame and the given list have different number of rows
         with pytest.raises(ValueError, match="different number of rows"):
             self.world.explore(column=np.array([1, 2, 3]))
 
diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py
index 89a30b45..ee947354 100644
--- a/geopandas/tests/test_geom_methods.py
+++ b/geopandas/tests/test_geom_methods.py
@@ -898,6 +898,41 @@ class TestGeomMethods:
         expected = Series(["Self-intersection[0.5 0.5]", "Valid Geometry", None])
         assert_series_equal(s.is_valid_reason(), expected)
 
+    @pytest.mark.skipif(
+        not (GEOS_GE_312 and SHAPELY_GE_21), reason="GEOS 3.12 and shapely 2.1 needed."
+    )
+    def test_is_valid_coverage(self):
+        s = GeoSeries(
+            [
+                Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+                Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]),
+            ]
+        )
+        assert s.is_valid_coverage()
+
+        s2 = GeoSeries(
+            [
+                Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+                Polygon([(0, 0), (0.5, 0.5), (1, 1), (0, 1), (0, 0)]),
+            ]
+        )
+        assert not s2.is_valid_coverage()
+
+    @pytest.mark.skipif(
+        not (GEOS_GE_312 and SHAPELY_GE_21), reason="GEOS 3.12 and shapely 2.1 needed."
+    )
+    def test_invalid_coverage_edges(self):
+        s = GeoSeries(
+            [
+                Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
+                Polygon([(0, 0), (0.5, 0.5), (1, 1), (0, 1), (0, 0)]),
+            ]
+        )
+        expected = GeoSeries(
+            [LineString([(0, 0), (1, 1)]), LineString([(0, 0), (0.5, 0.5), (1, 1)])]
+        )
+        assert_geoseries_equal(s.invalid_coverage_edges(), expected)
+
     def test_is_empty(self):
         expected = Series(np.array([False] * len(self.g1)), self.g1.index)
         self._test_unary_real("is_empty", expected, self.g1)