diff --git a/arbor/include/arbor/morph/place_pwlin.hpp b/arbor/include/arbor/morph/place_pwlin.hpp
index da278fab7804ee74546da648dd34ff572d312fbc..223c287acbdf0b9f60bfc17bc0655aa0b6ed06e4 100644
--- a/arbor/include/arbor/morph/place_pwlin.hpp
+++ b/arbor/include/arbor/morph/place_pwlin.hpp
@@ -4,6 +4,7 @@
 // sample points and interpolating linearly.
 
 #include <cmath>
+#include <limits>
 #include <utility>
 
 #include <arbor/morph/morphology.hpp>
@@ -84,6 +85,9 @@ struct place_pwlin {
     // Maximal set of segments or part segments whose union is coterminous with extent.
     std::vector<msegment> all_segments(const mextent& extent) const;
 
+    // The closest location to p. Returns the location and its distance from the input coordinates.
+    std::pair<mlocation, double> closest(double x, double y, double z) const;
+
 private:
     std::shared_ptr<place_pwlin_data> data_;
 };
diff --git a/arbor/morph/place_pwlin.cpp b/arbor/morph/place_pwlin.cpp
index 9814aeb22c8f7fb01c0639a1b4fb9fdec3cadbe5..f42475bcfcdc38f6de857884a242ff734bc70228 100644
--- a/arbor/morph/place_pwlin.cpp
+++ b/arbor/morph/place_pwlin.cpp
@@ -1,10 +1,14 @@
+#include <iostream>
+
 #include <cmath>
+#include <limits>
 #include <memory>
 #include <vector>
 
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/place_pwlin.hpp>
 #include <arbor/morph/primitives.hpp>
+#include <arbor/math.hpp>
 
 #include "util/piecewise.hpp"
 #include "util/rangeutil.hpp"
@@ -170,4 +174,82 @@ place_pwlin::place_pwlin(const arb::morphology& m, const isometry& iso) {
     }
 };
 
+struct p3d {
+    double x=0,y=0,z=0;
+    constexpr p3d() = default;
+    constexpr p3d(const mpoint& p): x(p.x), y(p.y), z(p.z) {}
+    constexpr p3d(double x, double y, double z): x(x), y(y), z(z) {}
+    friend constexpr p3d operator-(const p3d& l, const p3d& r) {
+        return {l.x-r.x, l.y-r.y, l.z-r.z};
+    }
+    friend constexpr p3d operator+(const p3d& l, const p3d& r) {
+        return {l.x+r.x, l.y+r.y, l.z+r.z};
+    }
+    friend constexpr p3d operator*(double l, const p3d& r) {
+        return {l*r.x, l*r.y, l*r.z};
+    }
+    friend constexpr double dot(const p3d& l, const p3d& r) {
+        return l.x*r.x + l.y*r.y + l.z*r.z;
+    }
+    friend double norm(const p3d p) {
+        return std::sqrt(dot(p, p));
+    }
+    friend std::ostream& operator<<(std::ostream& o, const p3d& p) {
+        return o << '[' << p.x << ' ' << p.y << ' ' << p.z << ']';
+    }
+};
+
+// Policy:
+//  If two collated points are equidistant from the input point, take the
+//  proximal location.
+// Rationale:
+//  if the location is on a fork point, it makes sense to take the proximal
+//  location, which corresponds to the end of the parent branch.
+std::pair<mlocation, double> place_pwlin::closest(double x, double y, double z) const {
+    double mind = std::numeric_limits<double>::max();
+    p3d p(x,y,z);
+    mlocation loc;
+
+    // loop over each branch
+    for (msize_t bid: util::count_along(data_->segment_index)) {
+        const auto b = data_->segment_index[bid];
+        // loop over the segments in the branch
+        for (auto s: b) {
+            const auto& seg = data_->segments[s.value];
+
+            // v and w are the proximal and distal ends of the segment.
+            const p3d v = seg.prox;
+            const p3d w = seg.dist;
+
+            const p3d vw = w-v;
+            const p3d vp = p-v;
+            const double wvs = dot(vw, vw);
+            if (wvs==0.) { // zero length segment is a special case
+                const double distance = norm(p-v);
+                if (distance<mind) {
+                    mind = distance;
+                    loc = {bid, s.lower_bound()};
+                }
+            }
+            else {
+                // Find the relative position of the orthogonal projection onto the line segment
+                // that along the axis of the segment:
+                //   t=0 -> proximal end of the segment
+                //   t=1 -> distal end of the segment
+                // values are clamped to the range [0, 1]
+                const double t = std::max(0., std::min(1., dot(vw, vp) / wvs));
+                const double distance =
+                    t<=0.? norm(p-v):
+                    t>=1.? norm(p-w):
+                           norm(p-(v + t*vw));
+                if (distance<mind) {
+                    loc = {bid, math::lerp(s.lower_bound(), s.upper_bound(), t)};
+                    mind = distance;
+                }
+            }
+        }
+    }
+    return {loc, mind};
+}
+
 } // namespace arb
diff --git a/doc/cpp/morphology.rst b/doc/cpp/morphology.rst
index 581290e2e8b57ab6a7bc98c3ca349fff7115bef6..dc432b625b7c9b0d4998318aad2ba326dc9a72ff 100644
--- a/doc/cpp/morphology.rst
+++ b/doc/cpp/morphology.rst
@@ -183,6 +183,10 @@ zero-length components as a result of such discontinuities.
       Return the maximal set of segments and partial segments whose
       union is coterminous with the given :cpp:class:`mextent` in the placement.
 
+   .. cpp:function:: closest(double x, double y, double z) -> std::pair<mpoint, double>
+
+      Find the closest location to p. Returns the location and its distance from the input coordinates.
+
 Isometries
 ^^^^^^^^^^
 
diff --git a/doc/python/morphology.rst b/doc/python/morphology.rst
index 972cf903b6a61e197ba2472f34ac1ac9c97d6338..6e233e183835029186b7255006789d8fd23112df 100644
--- a/doc/python/morphology.rst
+++ b/doc/python/morphology.rst
@@ -349,6 +349,10 @@ Cable cell morphology
        union is coterminous with the sub-region of the morphology covered by
        the given cables in the placement.
 
+    .. py:method:: closest(x: real, y: real, z: real) -> tuple[mpoint, real]
+
+        Find the closest location to p. Returns the location and its distance from the input coordinates.
+
 .. py:class:: isometry
 
     Isometries represent rotations and translations in space, and can be used with
diff --git a/python/morphology.cpp b/python/morphology.cpp
index 5a99baadd449c64c08214888ddd0049082321b53..a5d3fb0d15804616503c23b69c9109b67a5d19ca 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -199,7 +199,14 @@ void register_morphology(py::module& m) {
                 return self.all_segments(cables);
             },
             "Return maximal list of non-overlapping full or partial msegments whose union is coterminous "
-            "with the extent of the given list of cables.");
+            "with the extent of the given list of cables.")
+        .def("closest",
+            [](const arb::place_pwlin& self, double x, double y, double z) {
+                auto [l, d] = self.closest(x, y, z);
+                return pybind11::make_tuple(l, d);
+            },
+            "Find the location on the morphology that is closest to a 3d point. "
+            "Returns the location and its distance from the point.");
 
     //
     // Higher-level data structures (segment_tree, morphology)
diff --git a/test/unit/test_morph_place.cpp b/test/unit/test_morph_place.cpp
index e7869fa705f5aca08871550b1e545e17ca2404b8..fff37b7461c7da3a956cdaba9d8409f903e6256e 100644
--- a/test/unit/test_morph_place.cpp
+++ b/test/unit/test_morph_place.cpp
@@ -570,3 +570,104 @@ TEST(place_pwlin, segments) {
     EXPECT_TRUE(mpoint_almost_eq(p8p, x3all[2].prox));
     EXPECT_TRUE(mpoint_almost_eq(p8p, x3all[2].dist));
 }
+
+TEST(place_pwlin, nearest) {
+    segment_tree tree;
+
+    //  the test morphology:
+    //
+    //       x=-9        x=9
+    //
+    //         _        _  y=40
+    //          \       /
+    //      seg4 \     / seg2
+    //  branch 2  /   /     branch 1
+    //        seg3 \ /
+    //              | y=25
+    //              |
+    //              |
+    //              | branch 0
+    //        seg1  |
+    //              |
+    //              _ y=7
+    //
+    //              - y=5
+    //        seg0  |
+    //              _ y=-5
+
+    // Root branch.
+    mpoint psoma_p{0, -5, 0, 5};
+    mpoint psoma_d{0,  5, 0, 5};
+
+    msize_t ssoma = tree.append(mnpos, psoma_p, psoma_d, 1);
+
+    // Main leg of y, of length 28 μm
+    // Note that there is a gap of 2 μm between the end of the soma segment
+    mpoint py1_p{0,  7, 0, 1};
+    mpoint py1_d{0, 25, 0, 1};
+
+    msize_t sy1 = tree.append(ssoma, py1_p, py1_d, 3);
+
+    // branch 1 of y: translation (9,15) in one segment
+    mpoint py2_d{ 9, 40, 0, 1};
+    tree.append(sy1, py2_d, 3);
+
+    // branch 2 of y: translation (-9,15) in 2 segments
+    mpoint py3_m{-6, 35, 0, 1};
+    mpoint py3_d{-9, 40, 0, 1};
+    tree.append(tree.append(sy1, py3_m, 3), py3_d, 3);
+
+    morphology m(tree);
+    place_pwlin place(m);
+
+    {
+        auto [l, d] = place.closest(0, -5, 0);
+        EXPECT_EQ((mlocation{0, 0.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(10, -5, 0);
+        EXPECT_EQ((mlocation{0, 0.}), l);
+        EXPECT_EQ(10., d);
+    }
+    {
+        auto [l, d] = place.closest(0, 0, 0);
+        EXPECT_EQ((mlocation{0, 5./28.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(10, 0, 0);
+        EXPECT_EQ((mlocation{0, 5./28.}), l);
+        EXPECT_EQ(10., d);
+    }
+    {
+        auto [l, d] = place.closest(0, 25, 0);
+        EXPECT_EQ((mlocation{0, 1.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(0, 6, 0);
+        EXPECT_EQ((mlocation{0, 10./28.}), l);
+        EXPECT_EQ(1., d);
+    }
+    {
+        auto [l, d] = place.closest(3, 30, 0);
+        EXPECT_EQ((mlocation{1, 1./3.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(-6, 35, 0);
+        EXPECT_EQ((mlocation{2, 2./3.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(-9, 40, 0);
+        EXPECT_EQ((mlocation{2, 1.}), l);
+        EXPECT_EQ(0., d);
+    }
+    {
+        auto [l, d] = place.closest(-9, 41, 0);
+        EXPECT_EQ((mlocation{2, 1.}), l);
+        EXPECT_EQ(1., d);
+    }
+}