Fixed #34266 -- Added ClosestPoint GIS database functions.
diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py
index 287841b29e241..fafdf60743008 100644
--- a/django/contrib/gis/db/backends/base/operations.py
+++ b/django/contrib/gis/db/backends/base/operations.py
@@ -42,6 +42,7 @@ def select_extent(self):
         "Azimuth",
         "BoundingCircle",
         "Centroid",
+        "ClosestPoint",
         "Difference",
         "Distance",
         "Envelope",
diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py
index 0799a6b28f1b2..886db605cd504 100644
--- a/django/contrib/gis/db/backends/mysql/operations.py
+++ b/django/contrib/gis/db/backends/mysql/operations.py
@@ -75,6 +75,7 @@ def unsupported_functions(self):
             "AsSVG",
             "Azimuth",
             "BoundingCircle",
+            "ClosestPoint",
             "ForcePolygonCW",
             "GeometryDistance",
             "IsEmpty",
diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py
index c191d0b1f786f..b58af886de9fb 100644
--- a/django/contrib/gis/db/backends/oracle/operations.py
+++ b/django/contrib/gis/db/backends/oracle/operations.py
@@ -121,6 +121,7 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
         "AsKML",
         "AsSVG",
         "Azimuth",
+        "ClosestPoint",
         "ForcePolygonCW",
         "GeoHash",
         "GeometryDistance",
diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py
index d886f8916e989..19da355d28b34 100644
--- a/django/contrib/gis/db/models/functions.py
+++ b/django/contrib/gis/db/models/functions.py
@@ -280,6 +280,11 @@ class Centroid(OracleToleranceMixin, GeomOutputGeoFunc):
     arity = 1
 
 
+class ClosestPoint(GeomOutputGeoFunc):
+    arity = 2
+    geom_param_pos = (0, 1)
+
+
 class Difference(OracleToleranceMixin, GeomOutputGeoFunc):
     arity = 2
     geom_param_pos = (0, 1)
diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt
index d215eb1ecb15d..f8c0d83960faa 100644
--- a/docs/ref/contrib/gis/db-api.txt
+++ b/docs/ref/contrib/gis/db-api.txt
@@ -356,6 +356,7 @@ Function                              PostGIS  Oracle         MariaDB      MySQL
 :class:`Azimuth`                      X                                                X (LWGEOM/RTTOPO)
 :class:`BoundingCircle`               X        X
 :class:`Centroid`                     X        X              X            X           X
+:class:`ClosestPoint`                 X                                                X
 :class:`Difference`                   X        X              X            X           X
 :class:`Distance`                     X        X              X            X           X
 :class:`Envelope`                     X        X              X            X           X
diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt
index a07feebfb7d0b..963e9ed4bd434 100644
--- a/docs/ref/contrib/gis/functions.txt
+++ b/docs/ref/contrib/gis/functions.txt
@@ -26,10 +26,10 @@ Measurement                Relationships             Operations              Edi
 :class:`Area`              :class:`Azimuth`          :class:`Difference`     :class:`ForcePolygonCW`                      :class:`AsGeoJSON`  :class:`IsEmpty`
 :class:`Distance`          :class:`BoundingCircle`   :class:`Intersection`   :class:`MakeValid`                           :class:`AsGML`      :class:`IsValid`
 :class:`GeometryDistance`  :class:`Centroid`         :class:`SymDifference`  :class:`Reverse`                             :class:`AsKML`      :class:`MemSize`
-:class:`Length`            :class:`Envelope`         :class:`Union`          :class:`Scale`                               :class:`AsSVG`      :class:`NumGeometries`
-:class:`Perimeter`         :class:`LineLocatePoint`                          :class:`SnapToGrid`      :class:`FromWKB`    :class:`AsWKB`      :class:`NumPoints`
-                           :class:`PointOnSurface`                           :class:`Transform`       :class:`FromWKT`    :class:`AsWKT`
-                                                                             :class:`Translate`                           :class:`GeoHash`
+:class:`Length`            :class:`ClosestPoint`     :class:`Union`          :class:`Scale`                               :class:`AsSVG`      :class:`NumGeometries`
+:class:`Perimeter`         :class:`Envelope`                                 :class:`SnapToGrid`      :class:`FromWKB`    :class:`AsWKB`      :class:`NumPoints`
+                           :class:`LineLocatePoint`                          :class:`Transform`       :class:`FromWKT`    :class:`AsWKT`
+                           :class:`PointOnSurface`                           :class:`Translate`                           :class:`GeoHash`
 =========================  ========================  ======================  =======================  ==================  ==================  ======================
 
 ``Area``
@@ -237,6 +237,19 @@ The ``num_seg`` parameter is used only on PostGIS.
 Accepts a single geographic field or expression and returns the ``centroid``
 value of the geometry.
 
+``ClosestPoint``
+================
+
+.. versionadded:: 5.0
+
+.. class:: ClosestPoint(expr1, expr2, **extra)
+
+*Availability*: `PostGIS <https://postgis.net/docs/ST_ClosestPoint.html>`__,
+SpatiaLite
+
+Accepts two geographic fields or expressions and returns the 2-dimensional
+point on geometry A that is closest to geometry B.
+
 ``Difference``
 ==============
 
diff --git a/docs/releases/5.0.txt b/docs/releases/5.0.txt
index a1dc6a1c1c831..d1a4cba8707ea 100644
--- a/docs/releases/5.0.txt
+++ b/docs/releases/5.0.txt
@@ -64,7 +64,10 @@ Minor features
 :mod:`django.contrib.gis`
 ~~~~~~~~~~~~~~~~~~~~~~~~~
 
-* ...
+* The new
+  :class:`ClosestPoint() <django.contrib.gis.db.models.functions.ClosestPoint>`
+  function returns a 2-dimensional point on the geometry that is closest to
+  another geometry.
 
 :mod:`django.contrib.messages`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py
index 59b0410aca52a..e9e2a5dcaeebe 100644
--- a/tests/gis_tests/geoapp/test_functions.py
+++ b/tests/gis_tests/geoapp/test_functions.py
@@ -456,6 +456,18 @@ def test_area_lookups(self):
         ):
             qs.get(area__lt=500000)
 
+    @skipUnlessDBFeature("has_ClosestPoint_function")
+    def test_closest_point(self):
+        qs = Country.objects.annotate(
+            closest_point=functions.ClosestPoint("mpoly", functions.Centroid("mpoly"))
+        )
+        for country in qs:
+            self.assertIsInstance(country.closest_point, Point)
+            self.assertEqual(
+                country.mpoly.intersection(country.closest_point),
+                country.closest_point,
+            )
+
     @skipUnlessDBFeature("has_LineLocatePoint_function")
     def test_line_locate_point(self):
         pos_expr = functions.LineLocatePoint(

I'd like to suggest adding this function to the bunch to encourage people to prefer its use in a number of situations over [cumbersome solutions](https://niccolomineo.com/articles/marker-center-complex-line-geodjango/).