Repository: geopandas/geopandas

Issue:
REGR: overlay how='difference' shouldn't return empty df for non-overlapping dfs
- [ x] I have searched the [geopandas] tag on [StackOverflow](https://stackoverflow.com/questions/tagged/geopandas) and [GIS StackExchange](https://gis.stackexchange.com/questions/tagged/geopandas) for similar questions.

- [ x] I have asked my usage related question on [StackOverflow](https://stackoverflow.com) or [GIS StackExhange](https://gis.stackexchange.com).

---

#### Question about geopandas
I have posted my question on stack exchange here: https://stackoverflow.com/questions/69444069/is-overlay-how-difference-supposed-to-operate-differently-from-geopandas-0-9-v

When I use the overlay method/function using how='difference', I get different behaviors depending on the version of geopandas I'm using. For example

```
from shapely.geometry import Polygon
import geopandas

polys1 = geopandas.GeoSeries([Polygon([(0,0), (2,0), (2,2), (0,2)])])
polys2 = geopandas.GeoSeries([Polygon([(3,3), (5,3), (5,5), (3,5)])])

df1 = geopandas.GeoDataFrame({'geometry': polys1, 'df1':[1]})
df2 = geopandas.GeoDataFrame({'geometry': polys2, 'df2':[1]})

ax = df1.plot(color='red');
df2.plot(ax=ax, color='green', alpha=0.5);
```
![image](https://user-images.githubusercontent.com/18354318/136086268-72128684-db1f-4cf7-9204-ceab58b68864.png)

Where both geopandas 0.9 and geopandas 0.10 have the same image. The difference occurs once I use overlay function/method. 
```
# Same result for res_difference in geopandas 0.10 using method or function
# res_difference = df1.overlay(df2, how='difference')
res_difference = geopandas.overlay(df1, df2, how='difference')
print(res_difference)

ax = res_difference.plot(cmap='tab10')
df1.plot(ax=ax, facecolor='none', edgecolor='k');
df2.plot(ax=ax, facecolor='none', edgecolor='k');
```
Where geopandas 0.10 shows 
![image](https://user-images.githubusercontent.com/18354318/136086721-b894b770-fb54-44b6-92d5-0ac4f173dea7.png)

and geopandas 0.9 shows
![image](https://user-images.githubusercontent.com/18354318/136086944-7cfc65bf-2ff3-44e1-b265-f002d61c794f.png)

Is this the intention of difference to return empty dataframes if the polygons don't intersect and you do an overlay how='difference?






Discussion:
Whoops. No, this was not an intention. We have added a check based on the total bounds but didn't think it through properly. This is a regression that needs to be fixed. 

Thanks for the report!

Resolving patch:
diff --git a/geopandas/tools/overlay.py b/geopandas/tools/overlay.py
--- a/geopandas/tools/overlay.py
+++ b/geopandas/tools/overlay.py
@@ -59,11 +59,15 @@ def _overlay_intersection(df1, df2):
 
         return GeoDataFrame(dfinter, geometry=geom_intersect, crs=df1.crs)
     else:
-        return GeoDataFrame(
-            [],
-            columns=list(set(df1.columns).union(df2.columns)) + ["__idx1", "__idx2"],
-            crs=df1.crs,
+        result = df1.iloc[:0].merge(
+            df2.iloc[:0].drop(df2.geometry.name, axis=1),
+            left_index=True,
+            right_index=True,
+            suffixes=("_1", "_2"),
         )
+        return result[
+            result.columns.drop(df1.geometry.name).tolist() + [df1.geometry.name]
+        ]
 
 
 def _overlay_difference(df1, df2):
@@ -265,23 +269,23 @@ def overlay(df1, df2, how="intersection", keep_geom_type=None, make_valid=True):
                 "df{} contains mixed geometry types.".format(i + 1)
             )
 
-    box_gdf1 = df1.total_bounds
-    box_gdf2 = df2.total_bounds
-
-    if not (
-        ((box_gdf1[0] <= box_gdf2[2]) and (box_gdf2[0] <= box_gdf1[2]))
-        and ((box_gdf1[1] <= box_gdf2[3]) and (box_gdf2[1] <= box_gdf1[3]))
-    ):
-        return GeoDataFrame(
-            [],
-            columns=list(
-                set(
-                    df1.drop(df1.geometry.name, axis=1).columns.to_list()
-                    + df2.drop(df2.geometry.name, axis=1).columns.to_list()
-                )
+    if how == "intersection":
+        box_gdf1 = df1.total_bounds
+        box_gdf2 = df2.total_bounds
+
+        if not (
+            ((box_gdf1[0] <= box_gdf2[2]) and (box_gdf2[0] <= box_gdf1[2]))
+            and ((box_gdf1[1] <= box_gdf2[3]) and (box_gdf2[1] <= box_gdf1[3]))
+        ):
+            result = df1.iloc[:0].merge(
+                df2.iloc[:0].drop(df2.geometry.name, axis=1),
+                left_index=True,
+                right_index=True,
+                suffixes=("_1", "_2"),
             )
-            + ["geometry"],
-        )
+            return result[
+                result.columns.drop(df1.geometry.name).tolist() + [df1.geometry.name]
+            ]
 
     # Computations
     def _make_valid(df):


Test patch:
diff --git a/geopandas/tests/test_overlay.py b/geopandas/tests/test_overlay.py
--- a/geopandas/tests/test_overlay.py
+++ b/geopandas/tests/test_overlay.py
@@ -1,6 +1,7 @@
 import os
 from distutils.version import LooseVersion
 
+import numpy as np
 import pandas as pd
 
 from shapely.geometry import Point, Polygon, LineString, GeometryCollection, box
@@ -374,7 +375,7 @@ def test_empty_intersection(dfs):
     df3 = GeoDataFrame({"geometry": polys3, "col3": [1, 2]})
     expected = GeoDataFrame([], columns=["col1", "col3", "geometry"])
     result = overlay(df1, df3)
-    assert_geodataframe_equal(result, expected, check_like=True)
+    assert_geodataframe_equal(result, expected, check_dtype=False)
 
 
 def test_correct_index(dfs):
@@ -656,5 +657,69 @@ def test_empty_overlay_return_non_duplicated_columns():
 
     result = geopandas.overlay(nybb, nybb2)
 
-    assert all(result.columns.isin(nybb.columns))
-    assert len(result.columns) == len(nybb.columns)
+    expected = GeoDataFrame(
+        columns=[
+            "BoroCode_1",
+            "BoroName_1",
+            "Shape_Leng_1",
+            "Shape_Area_1",
+            "BoroCode_2",
+            "BoroName_2",
+            "Shape_Leng_2",
+            "Shape_Area_2",
+            "geometry",
+        ],
+        crs=nybb.crs,
+    )
+    assert_geodataframe_equal(result, expected, check_dtype=False)
+
+
+def test_non_overlapping(how):
+    p1 = Polygon([(0, 0), (2, 0), (2, 2), (0, 2)])
+    p2 = Polygon([(3, 3), (5, 3), (5, 5), (3, 5)])
+    df1 = GeoDataFrame({"col1": [1], "geometry": [p1]})
+    df2 = GeoDataFrame({"col2": [2], "geometry": [p2]})
+    result = overlay(df1, df2, how=how)
+
+    if how == "intersection":
+        expected = GeoDataFrame(
+            {
+                "col1": np.array([], dtype="int64"),
+                "col2": np.array([], dtype="int64"),
+                "geometry": [],
+            },
+            index=pd.Index([], dtype="object"),
+        )
+    elif how == "union":
+        expected = GeoDataFrame(
+            {
+                "col1": [1, np.nan],
+                "col2": [np.nan, 2],
+                "geometry": [p1, p2],
+            }
+        )
+    elif how == "identity":
+        expected = GeoDataFrame(
+            {
+                "col1": [1.0],
+                "col2": [np.nan],
+                "geometry": [p1],
+            }
+        )
+    elif how == "symmetric_difference":
+        expected = GeoDataFrame(
+            {
+                "col1": [1, np.nan],
+                "col2": [np.nan, 2],
+                "geometry": [p1, p2],
+            }
+        )
+    elif how == "difference":
+        expected = GeoDataFrame(
+            {
+                "col1": [1],
+                "geometry": [p1],
+            }
+        )
+
+    assert_geodataframe_equal(result, expected)