diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index 19a68d2eb26a990b8858b359216a2c75c8fd8b2f..dc6364325399b3f7a604a5d95a0317e4334bc241 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -1,3 +1,4 @@
+#include <algorithm>
 #include <set>
 #include <stdexcept>
 #include <unordered_set>
@@ -19,6 +20,7 @@
 #include "util/piecewise.hpp"
 #include "util/rangeutil.hpp"
 #include "util/transform.hpp"
+#include "util/unique.hpp"
 
 namespace arb {
 
@@ -30,6 +32,7 @@ using util::pw_elements;
 using util::pw_element;
 using util::sort;
 using util::sort_by;
+using util::stable_sort_by;
 using util::value_by_key;
 
 namespace {
@@ -124,43 +127,52 @@ pw_elements<U> pw_over_cable(const mcable_map<T>& mm, mcable cable, U dflt_value
 
 // Construct cv_geometry for cell from locset describing CV boundary points.
 cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset) {
-    struct mloc_hash {
-        std::size_t operator()(const mlocation& loc) const { return loc.branch ^ std::hash<double>()(loc.pos); }
-    };
-    using mlocation_map = std::unordered_map<mlocation, fvm_size_type, mloc_hash>;
     auto pop = [](auto& vec) { auto h = vec.back(); return vec.pop_back(), h; };
 
     cv_geometry geom;
     const auto& mp = cell.provider();
     const auto& m = mp.morphology();
 
-    if (mp.morphology().empty()) {
+    if (m.empty()) {
         geom.cell_cv_divs = {0, 0};
         return geom;
     }
 
-    auto canon =    [&m](mlocation loc)  { return canonical(m, loc); };
-    auto origin =   [&canon](mcable cab) { return canon(mlocation{cab.branch, cab.prox_pos}); };
-    auto terminus = [&canon](mcable cab) { return canon(mlocation{cab.branch, cab.dist_pos}); };
-
     mlocation_list locs = thingify(lset, mp);
 
-    std::vector<msize_t> n_cv_cables;
-    std::vector<mlocation> next_cv_head;
-    next_cv_head.push_back({mnpos, 0});
+    // Filter out root, terminal locations and repeated locations so as to
+    // avoid trivial CVs outside of fork points. (This is not necessary for
+    // correctness, but is for the convenience of specification by lset.)
 
-    mcable_list cables, all_cables;
-    std::vector<msize_t> branches;
+    auto neither_root_nor_terminal = [&m](mlocation x) {
+        return !(x.pos==0 && x.branch==(m.branch_children(mnpos).size()>1u? mnpos: 0)) // root?
+            && !(x.pos==1 && m.branch_children(x.branch).empty()); // terminal?
+    };
+    locs.erase(std::partition(locs.begin(), locs.end(), neither_root_nor_terminal), locs.end());
+    util::sort(locs);
+    util::unique_in_place(locs);
 
-    mlocation_map head_count;
-    unsigned extra_cv_count = 0;
+    // Collect cables constituting each CV, maintaining a stack of CV
+    // proximal 'head' points, and recursing down branches in the morphology
+    // within each CV.
+
+    constexpr fvm_index_type no_parent = -1;
+    std::vector<std::pair<mlocation, fvm_index_type>> next_cv_head; // head loc, parent cv index
+    next_cv_head.emplace_back(mlocation{mnpos, 0}, no_parent);
+
+    mcable_list cables;
+    std::vector<msize_t> branches;
+    geom.cv_cables_divs.push_back(0);
+    fvm_index_type cv_index = 0;
 
     while (!next_cv_head.empty()) {
-        mlocation h = pop(next_cv_head);
+        auto next = pop(next_cv_head);
+        mlocation h = next.first;
 
         cables.clear();
         branches.clear();
         branches.push_back(h.branch);
+        geom.cv_parent.push_back(next.second);
 
         while (!branches.empty()) {
             msize_t b = pop(branches);
@@ -178,7 +190,7 @@ cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset) {
             // Otherwise, recurse over child branches.
             if (it!=locs.end() && it->branch==b) {
                 cables.push_back({b, b==h.branch? h.pos: 0, it->pos});
-                next_cv_head.push_back(*it);
+                next_cv_head.emplace_back(*it, cv_index);
             }
             else {
                 if (b!=mnpos) {
@@ -190,69 +202,33 @@ cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset) {
             }
         }
 
-        auto empty_cable = [](mcable c) { return c.prox_pos==c.dist_pos; };
-        cables.erase(std::remove_if(cables.begin(), cables.end(), empty_cable), cables.end());
-
-        if (!cables.empty()) {
-            if (++head_count[origin(cables.front())]==2) {
-                ++extra_cv_count;
-            }
-
-            n_cv_cables.push_back(cables.size());
-            sort(cables);
-            append(all_cables, std::move(cables));
-        }
-    }
-
-    geom.cv_cables.reserve(all_cables.size()+extra_cv_count);
-    geom.cv_parent.reserve(n_cv_cables.size()+extra_cv_count);
-    geom.cv_cables_divs.reserve(n_cv_cables.size()+extra_cv_count+1);
-    geom.cv_cables_divs.push_back(0);
-
-    mlocation_map parent_map;
-
-    unsigned all_cables_index = 0;
-    unsigned cv_index = 0;
-
-    // Multiple CVs meeting at (0,0)?
-    mlocation root{0, 0};
-    unsigned n_top_children = value_by_key(head_count, root).value_or(0);
-    if (n_top_children>1) {
-        // Add initial trical CV.
-        geom.cv_parent.push_back(mnpos);
-        geom.cv_cables.push_back(mcable{0, 0, 0});
+        sort(cables);
+        append(geom.cv_cables, std::move(cables));
         geom.cv_cables_divs.push_back(geom.cv_cables.size());
-        parent_map[root] = cv_index++;
+        ++cv_index;
     }
 
-    for (auto n_cables: n_cv_cables) {
-        mlocation head = origin(all_cables[all_cables_index]);
-        msize_t parent_cv = value_by_key(parent_map, head).value_or(mnpos);
+    auto n_cv = cv_index;
+    arb_assert(n_cv>0);
+    arb_assert(geom.cv_parent.front()==-1);
+    arb_assert(util::all_of(util::subrange_view(geom.cv_parent, 1, n_cv),
+            [](auto v) { return v!=no_parent; }));
 
-        auto cables = util::subrange_view(all_cables, all_cables_index, all_cables_index+n_cables);
-        std::copy(cables.begin(), cables.end(), std::back_inserter(geom.cv_cables));
+    // Construct CV children mapping by sorting CV indices by parent.
+    assign(geom.cv_children, util::make_span(1, n_cv));
+    stable_sort_by(geom.cv_children, [&geom](auto cv) { return geom.cv_parent[cv]; });
 
-        geom.cv_parent.push_back(parent_cv);
-        geom.cv_cables_divs.push_back(geom.cv_cables.size());
+    geom.cv_children_divs.reserve(n_cv+1);
+    geom.cv_children_divs.push_back(0);
 
-        auto this_cv = cv_index++;
-        for (auto cable: cables) {
-            mlocation term = terminus(cable);
-
-            unsigned n_children = value_by_key(head_count, term).value_or(0);
-            if (n_children>1) {
-                // Add trivial CV for lindep.
-                geom.cv_parent.push_back(this_cv);
-                geom.cv_cables.push_back(mcable{cable.branch, 1., 1.});
-                geom.cv_cables_divs.push_back((fvm_index_type)geom.cv_cables.size());
-                parent_map[term] = cv_index++;
-            }
-            else {
-                parent_map[term] = this_cv;
-            }
-        }
+    auto b = geom.cv_children.begin();
+    auto e = geom.cv_children.end();
+    auto from = b;
 
-        all_cables_index += n_cables;
+    for (fvm_index_type cv = 0; cv<n_cv; ++cv) {
+        from = std::partition_point(from, e,
+            [cv, &geom](auto i) { return geom.cv_parent[i]<=cv; });
+        geom.cv_children_divs.push_back(from-b);
     }
 
     // Fill cv/cell mapping for single cell (index 0).
@@ -263,17 +239,14 @@ cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset) {
     geom.branch_cv_map.resize(1);
     std::vector<pw_elements<fvm_size_type>>& bmap = geom.branch_cv_map.back();
 
-    for (auto cv: util::make_span(geom.size())) {
+    for (auto cv: util::make_span(n_cv)) {
         for (auto cable: geom.cables(cv)) {
             if (cable.branch>=bmap.size()) {
                 bmap.resize(cable.branch+1);
             }
 
             // Ordering of CV ensures CV cables on any given branch are found sequentially.
-            // Omit empty cables.
-            if (cable.prox_pos<cable.dist_pos) {
-                bmap[cable.branch].push_back(cable.prox_pos, cable.dist_pos, cv);
-            }
+            bmap[cable.branch].push_back(cable.prox_pos, cable.dist_pos, cv);
         }
     }
 
@@ -327,6 +300,9 @@ cv_geometry& append(cv_geometry& geom, const cv_geometry& right) {
     append_divs(geom.cv_cables_divs, right.cv_cables_divs);
 
     append_offset(geom.cv_parent, geom_n_cv, right.cv_parent);
+    append_offset(geom.cv_children, geom_n_cv, right.cv_children);
+    append_divs(geom.cv_children_divs, right.cv_children_divs);
+
     append_offset(geom.cv_to_cell, geom_n_cell, right.cv_to_cell);
     append_divs(geom.cell_cv_divs, right.cell_cv_divs);
 
@@ -345,6 +321,8 @@ fvm_cv_discretization& append(fvm_cv_discretization& dczn, const fvm_cv_discreti
     append(dczn.temperature_K, right.temperature_K);
     append(dczn.diam_um, right.diam_um);
 
+    append(dczn.axial_resistivity, right.axial_resistivity);
+
     return dczn;
 }
 
@@ -372,13 +350,21 @@ fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell
     double dflt_potential =   *(dflt.init_membrane_potential | global_dflt.init_membrane_potential);
     double dflt_temperature = *(dflt.temperature_K | global_dflt.temperature_K);
 
+    D.axial_resistivity.resize(1);
+    msize_t n_branch = D.geometry.n_branch(0);
+    D.axial_resistivity[0].reserve(n_branch);
+    for (msize_t i = 0; i<n_branch; ++i) {
+        D.axial_resistivity[0].push_back(pw_over_cable(cell.region_assignments().get<axial_resistivity>(),
+                    mcable{i, 0., 1.}, dflt_resistivity));
+    }
+
     const auto& embedding = cell.embedding();
     for (auto i: count_along(D.geometry.cv_parent)) {
         auto cv_cables = D.geometry.cables(i);
 
         // Computing face_conductance:
         //
-        // Flux between adjacemt CVs is computed as if there were no membrane currents, and with the CV voltage
+        // Flux between adjacent CVs is computed as if there were no membrane currents, and with the CV voltage
         // values taken to be exact at a reference point in each CV:
         //     * If the CV is unbranched, the reference point is taken to be the CV midpoint.
         //     * If the CV is branched, the reference point is taken to be closest branch point to
@@ -408,8 +394,7 @@ fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell
             }
 
             mcable span{bid, parent_refpt, cv_refpt};
-            double resistance = embedding.integrate_ixa(bid,
-                pw_over_cable(cell.region_assignments().get<axial_resistivity>(), span, dflt_resistivity));
+            double resistance = embedding.integrate_ixa(span, D.axial_resistivity[0].at(bid));
             D.face_conductance[i] = 100/resistance; // 100 scales to µS.
         }
 
@@ -718,7 +703,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
             std::sort(in.param_value.begin(), in.param_value.end());
 
             in.target_index = pm.lid;
-            in.cv = D.geometry.location_cv(cell_idx, pm.loc);
+            in.cv = D.geometry.location_cv(cell_idx, pm.loc, cv_prefer::cv_nonempty);
             sl.push_back(std::move(in));
         }
 
@@ -781,7 +766,8 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
         const auto& stimuli = cell.stimuli();
 
         std::vector<size_type> stimuli_cv;
-        assign_by(stimuli_cv, stimuli, [&D, cell_idx](auto& p) { return D.geometry.location_cv(cell_idx, p.loc); });
+        assign_by(stimuli_cv, stimuli, [&D, cell_idx](auto& p) {
+                return D.geometry.location_cv(cell_idx, p.loc, cv_prefer::cv_nonempty); });
 
         std::vector<size_type> cv_order;
         assign(cv_order, count_along(stimuli));
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index 1c5566f81482c94f8941ed2f5bf49f7d3deaa418..434ba6f5e80363941fe43f9e064e8480e5a3529d 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -4,12 +4,12 @@
 #include <utility>
 #include <vector>
 
-#include <arbor/fvm_types.hpp>
 #include <arbor/cable_cell.hpp>
 #include <arbor/mechanism.hpp>
 #include <arbor/mechinfo.hpp>
 #include <arbor/mechcat.hpp>
 #include <arbor/recipe.hpp>
+#include <arbor/util/optional.hpp>
 
 #include "execution_context.hpp"
 #include "util/piecewise.hpp"
@@ -19,15 +19,52 @@
 namespace arb {
 
 // CV geometry as determined by per-cell CV boundary points.
+//
+// Details of CV cable representation:
+//
+//   * The extent of the cables of a control volume corresponds to the
+//     closure of the control volume on the morphology tree.
+//
+//   * Every fork in the morphology tree 'belongs' to exactly
+//     one CV. A fork belongs to a CV if and only if there is
+//     a cable in the CV for each branch of the fork, that
+//     includes the fork point.
+//
+//   * If a CV has more than one cable covering a fork point, then
+//     that fork point must belong to that CV.
+//
+// These requirements are a consequence of the CV geometry being determined
+// by a collection of CV boundarary locations.
+
+namespace cv_prefer {
+    // Enum for resolving which CV to return on location look-up, if the
+    // location is on a CV boundary.
+
+    enum type {
+        // Prefer more proximal CV:
+        cv_proximal,
+        // Prefer more distal CV:
+        cv_distal,
+        // Prefer distal CV unless it has zero extent on location branch.
+        // This should be used for placing point processes on CVs.
+        cv_nonempty,
+        // Prefer distal CV unless the proximal CV has zero extent on location branch.
+        // This should be used for determing to which CV a fork point belongs.
+        cv_empty
+    };
+}
 
 struct cv_geometry {
     using size_type = fvm_size_type;
     using index_type = fvm_index_type;
 
     std::vector<mcable> cv_cables;           // CV unbranched sections, partitioned by CV.
-    std::vector<index_type> cv_cables_divs;   // Partitions cv_cables by CV index.
+    std::vector<index_type> cv_cables_divs;  // Partitions cv_cables by CV index.
     std::vector<index_type> cv_parent;       // Index of CV parent or size_type(-1) for a cell root CV.
 
+    std::vector<index_type> cv_children;     // CV child indices, partitioned by CV, and then in order.
+    std::vector<index_type> cv_children_divs;   // Paritions cv_children by CV index.
+
     std::vector<index_type> cv_to_cell;      // Maps CV index to cell index.
     std::vector<index_type> cell_cv_divs;    // Partitions CV indices by cell.
 
@@ -39,6 +76,11 @@ struct cv_geometry {
         return util::subrange_view(cv_cables, partn[cv_index]);
     }
 
+    auto children(size_type cv_index) const {
+        auto partn = util::partition_view(cv_children_divs);
+        return util::subrange_view(cv_children, partn[cv_index]);
+    }
+
     std::pair<index_type, index_type> cell_cv_interval(size_type cell_idx) const {
         auto partn = util::partition_view(cell_cv_divs);
         return partn[cell_idx];
@@ -68,8 +110,43 @@ struct cv_geometry {
         return cell_cv_divs.empty()? 0: cell_cv_divs.size()-1;
     }
 
-    size_type location_cv(size_type cell_idx, mlocation loc) const {
-        return cell_cv_divs.at(cell_idx)+branch_cv_map.at(cell_idx).at(loc.branch)(loc.pos).second;
+    size_type n_branch(size_type cell_idx) const {
+        return branch_cv_map.at(cell_idx).size();
+    }
+
+    size_type location_cv(size_type cell_idx, mlocation loc, cv_prefer::type prefer) const {
+        auto& pw_cv_offset = branch_cv_map.at(cell_idx).at(loc.branch);
+        auto zero_extent = [&pw_cv_offset](auto j) {
+            return pw_cv_offset.interval(j).first==pw_cv_offset.interval(j).second;
+        };
+
+        auto i = pw_cv_offset.index_of(loc.pos);
+        auto i_max = pw_cv_offset.size()-1;
+        auto cv_prox = pw_cv_offset.interval(i).first;
+
+        // index_of() should have returned right-most matching interval.
+        arb_assert(i==i_max || loc.pos<pw_cv_offset.interval(i+1).first);
+
+        using namespace cv_prefer;
+        switch (prefer) {
+        case cv_distal:
+            break;
+        case cv_proximal:
+            if (loc.pos==cv_prox && i>0) --i;
+            break;
+        case cv_nonempty:
+            if (zero_extent(i)) {
+                if (i>0 && !zero_extent(i-1)) --i;
+                else if (i<i_max && !zero_extent(i+1)) ++i;
+            }
+            break;
+        case cv_empty:
+            if (loc.pos==cv_prox && i>0 && zero_extent(i-1)) --i;
+            break;
+        }
+
+        index_type cv_base = cell_cv_divs.at(cell_idx);
+        return cv_base+pw_cv_offset[i].second;
     }
 };
 
@@ -86,6 +163,14 @@ cv_geometry cv_geometry_from_ends(const cable_cell& cell, const locset& lset);
 // diam_um is taken to be the diameter of a CV with constant diameter and same
 // extent which has the same surface area (i.e. cv_area/(Ï€L) where L is the
 // total length of the cables comprising the CV.)
+//
+// The bulk conductivity over the morphology is recorded here as well, for
+// calculating voltage and axial current interpolating probes.
+//
+// For the computation of inter-CV conductances and voltage interpolation, it
+// is assumed that the CV voltage is exact at every fork point that belongs
+// to the CV, or in the absence of any internal forks, is exact at the
+// midpoint of an unbranched CV.
 
 struct fvm_cv_discretization {
     using size_type = fvm_size_type;
@@ -98,12 +183,16 @@ struct fvm_cv_discretization {
     size_type size() const { return geometry.size(); }
     size_type n_cell() const { return geometry.n_cell(); }
 
+    // Following members have one element per CV.
     std::vector<value_type> face_conductance; // [µS]
     std::vector<value_type> cv_area;          // [µm²]
     std::vector<value_type> cv_capacitance;   // [pF]
     std::vector<value_type> init_membrane_potential; // [mV]
     std::vector<value_type> temperature_K;    // [K]
     std::vector<value_type> diam_um;          // [µm]
+
+    // For each cell, one piece-wise constant value per branch.
+    std::vector<std::vector<pw_constant_fn>> axial_resistivity; // [Ω·cm]
 };
 
 // Combine two fvm_cv_geometry groups in-place.
@@ -111,7 +200,6 @@ struct fvm_cv_discretization {
 fvm_cv_discretization& append(fvm_cv_discretization&, const fvm_cv_discretization&);
 
 // Construct fvm_cv_discretization from one or more cells.
-
 fvm_cv_discretization fvm_cv_discretize(const cable_cell& cell, const cable_cell_parameter_set& global_dflt);
 fvm_cv_discretization fvm_cv_discretize(const std::vector<cable_cell>& cells, const cable_cell_parameter_set& global_defaults, const arb::execution_context& ctx={});
 
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index 23ecd5035d56caeea5f8c8900959f8ef7456047e..0f54936bb7220964aa3c8d1ef0b1d577ff52ea95 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -520,7 +520,7 @@ void fvm_lowered_cell_impl<B>::initialize(
         cell_gid_type gid = gids[cell_idx];
 
         for (auto entry: cells[cell_idx].detectors()) {
-            detector_cv.push_back(D.geometry.location_cv(cell_idx, entry.loc));
+            detector_cv.push_back(D.geometry.location_cv(cell_idx, entry.loc, cv_prefer::cv_empty));
             detector_threshold.push_back(entry.item.threshold);
         }
 
@@ -528,14 +528,16 @@ void fvm_lowered_cell_impl<B>::initialize(
             probe_info pi = rec.get_probe({gid, j});
             auto where = any_cast<cell_probe_address>(pi.address);
 
-            auto cv = D.geometry.location_cv(cell_idx, where.location);
+            fvm_size_type cv;
             probe_handle handle;
 
             switch (where.kind) {
             case cell_probe_address::membrane_voltage:
+                cv = D.geometry.location_cv(cell_idx, where.location, cv_prefer::cv_empty);
                 handle = state_->voltage.data()+cv;
                 break;
             case cell_probe_address::membrane_current:
+                cv = D.geometry.location_cv(cell_idx, where.location, cv_prefer::cv_nonempty);
                 handle = state_->current_density.data()+cv;
                 break;
             default:
@@ -568,7 +570,7 @@ std::vector<fvm_gap_junction> fvm_lowered_cell_impl<B>::fvm_gap_junctions(
         const auto& cell_gj = cells[cell_idx].gap_junction_sites();
 
         for (auto gj : cell_gj) {
-            auto cv = D.geometry.location_cv(cell_idx, gj.loc);
+            auto cv = D.geometry.location_cv(cell_idx, gj.loc, cv_prefer::cv_nonempty);
             gid_to_cvs[gids[cell_idx]].push_back(cv);
         }
     }
diff --git a/arbor/include/arbor/morph/embed_pwlin.hpp b/arbor/include/arbor/morph/embed_pwlin.hpp
index 0338186bb31fa1b45cde63100ec2f7522c019cb1..0a352a5708029cfe2d5ee7e40ef81f3caa8d3aa7 100644
--- a/arbor/include/arbor/morph/embed_pwlin.hpp
+++ b/arbor/include/arbor/morph/embed_pwlin.hpp
@@ -36,15 +36,21 @@ struct embed_pwlin {
     // Computed length of mcable.
     double integrate_length(mcable c) const;
     double integrate_length(mlocation proxmal, mlocation distal) const;
+
+    double integrate_length(mcable c, const pw_constant_fn&) const;
     double integrate_length(msize_t bid, const pw_constant_fn&) const;
 
     // Membrane surface area of given mcable.
     double integrate_area(mcable c) const;
     double integrate_area(mlocation proxmal, mlocation distal) const;
+
+    double integrate_area(mcable c, const pw_constant_fn&) const;
     double integrate_area(msize_t bid, const pw_constant_fn&) const;
 
     // Integrated inverse cross-sectional area of given mcable.
     double integrate_ixa(mcable c) const;
+
+    double integrate_ixa(mcable c, const pw_constant_fn&) const;
     double integrate_ixa(msize_t bid, const pw_constant_fn&) const;
 
     // Length of whole branch.
diff --git a/arbor/morph/embed_pwlin.cpp b/arbor/morph/embed_pwlin.cpp
index eaf430a6f42bafc9fde9f7ae8d0c28eb30f059ad..ae3d0f2117a11f5d4425e4977c75361cc581e552 100644
--- a/arbor/morph/embed_pwlin.cpp
+++ b/arbor/morph/embed_pwlin.cpp
@@ -53,6 +53,36 @@ double integrate(const branch_pw_ratpoly<p, q>& f, unsigned bid, const pw_consta
     return accum;
 }
 
+// Performance note: when integrating over a cable within a branch, the code effectively
+// performs a linear search for the starting interval. This can be replaced with a binary
+// search for a small increase in code complexity.
+
+template <unsigned p, unsigned q>
+double integrate(const branch_pw_ratpoly<p, q>& f, mcable c, const pw_constant_fn& g) {
+    msize_t bid = c.branch;
+    double accum = 0;
+
+    for (msize_t i = 0; i<g.size(); ++i) {
+        std::pair<double, double> interval = g.interval(i);
+
+        if (interval.second<c.prox_pos) {
+            continue;
+        }
+        else if (interval.first>=c.dist_pos) {
+            break;
+        }
+        else {
+            interval.first = std::max(interval.first, c.prox_pos);
+            interval.second = std::min(interval.second, c.dist_pos);
+
+            if (interval.first<interval.second) {
+                accum += g.element(i)*(interpolate(f, bid, interval.second)-interpolate(f, bid, interval.first));
+            }
+        }
+    }
+    return accum;
+}
+
 template <typename operation>
 mcable_list data_cmp(const branch_pw_ratpoly<1, 0>& f, unsigned bid, double val, operation op) {
     mcable_list L;
@@ -109,19 +139,19 @@ double embed_pwlin::directed_projection(arb::mlocation loc) const {
     return interpolate(data_->directed_projection, loc.branch, loc.pos);
 }
 
-double embed_pwlin::integrate_length(msize_t bid, const pw_constant_fn& g) const {
-    return integrate(data_->length, bid, g);
-}
+// Point to point integration:
 
-double embed_pwlin::integrate_area(msize_t bid, const pw_constant_fn& g) const {
-    return integrate(data_->area, bid, g);
+double embed_pwlin::integrate_length(mlocation proximal, mlocation distal) const {
+    return interpolate(data_->length, distal.branch, distal.pos) -
+           interpolate(data_->length, proximal.branch, proximal.pos);
 }
 
-double embed_pwlin::integrate_ixa(msize_t bid, const pw_constant_fn& g) const {
-    return integrate(data_->ixa, bid, g);
+double embed_pwlin::integrate_area(mlocation proximal, mlocation distal) const {
+    return interpolate(data_->area, distal.branch, distal.pos) -
+           interpolate(data_->area, proximal.branch, proximal.pos);
 }
 
-// Cable versions of integration methods:
+// Integrate over cable:
 
 double embed_pwlin::integrate_length(mcable c) const {
     return integrate_length(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
@@ -135,16 +165,32 @@ double embed_pwlin::integrate_ixa(mcable c) const {
     return integrate_ixa(c.branch, pw_constant_fn{{c.prox_pos, c.dist_pos}, {1.}});
 }
 
-// Point to point integration:
+// Integrate piecewise function over a branch:
 
-double embed_pwlin::integrate_length(mlocation proximal, mlocation distal) const {
-    return interpolate(data_->length, distal.branch, distal.pos) -
-           interpolate(data_->length, proximal.branch, proximal.pos);
+double embed_pwlin::integrate_length(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->length, bid, g);
 }
 
-double embed_pwlin::integrate_area(mlocation proximal, mlocation distal) const {
-    return interpolate(data_->area, distal.branch, distal.pos) -
-           interpolate(data_->area, proximal.branch, proximal.pos);
+double embed_pwlin::integrate_area(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->area, bid, g);
+}
+
+double embed_pwlin::integrate_ixa(msize_t bid, const pw_constant_fn& g) const {
+    return integrate(data_->ixa, bid, g);
+}
+
+// Integrate piecewise function over a cable:
+
+double embed_pwlin::integrate_length(mcable c, const pw_constant_fn& g) const {
+    return integrate(data_->length, c, g);
+}
+
+double embed_pwlin::integrate_area(mcable c, const pw_constant_fn& g) const {
+    return integrate(data_->area, c, g);
+}
+
+double embed_pwlin::integrate_ixa(mcable c, const pw_constant_fn& g) const {
+    return integrate(data_->ixa, c, g);
 }
 
 // Subregions defined by geometric inequalities:
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index 54ad51e090af6acedec4b91e6ddb9a1afd378d15..ad6a9a4e6a4c33ede2c0806e7b646acb11c706db 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -16,6 +16,7 @@
 #include "util/transform.hpp"
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
+#include "util/unique.hpp"
 
 namespace arb {
 namespace ls {
@@ -203,21 +204,6 @@ locset most_distal(region reg) {
     return locset(most_distal_{std::move(reg)});
 }
 
-template <typename X>
-void unique_in_place(std::vector<X>& v) {
-    if (v.empty()) return;
-
-    auto write = v.begin();
-    auto read = write;
-
-    while (++read!=v.end()) {
-        if (*read==*write) continue;
-        if (++write!=read) *write = std::move(*read);
-    }
-
-    v.erase(++write, v.end());
-}
-
 mlocation_list thingify_(const most_distal_& n, const mprovider& p) {
     mlocation_list L;
 
@@ -244,7 +230,7 @@ mlocation_list thingify_(const most_distal_& n, const mprovider& p) {
     }
 
     util::sort(L);
-    unique_in_place(L);
+    util::unique_in_place(L);
     return L;
 }
 
diff --git a/arbor/util/unique.hpp b/arbor/util/unique.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..e55c122619157b8acb6aaa8efbdde3820c1d62eb
--- /dev/null
+++ b/arbor/util/unique.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+// Eliminated successive identical elements in-place in a SequenceConteiner.
+// (Can wrap or be replaced by std::unique in C++17.)
+//
+// If we ever implement a unique_view, it should go here too.
+
+#include <utility>
+
+namespace arb {
+namespace util {
+
+template <typename SeqContainer, typename EqPred = std::equal_to<>>
+void unique_in_place(SeqContainer& c, EqPred eq = EqPred{}) {
+    if (c.empty()) return;
+
+    auto end = c.end();
+    auto write = c.begin();
+    auto read = write;
+
+    while (++read!=end) {
+        if (eq(*read, *write)) continue;
+        if (++write!=read) *write = std::move(*read);
+    }
+
+    c.erase(++write, end);
+}
+
+} // namespace util
+} // namespace arb
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index f97bb2d3127688e8cefc639301a524b32552cdc5..d867d294b15d406f4f933ca16e181084b1507887 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -144,6 +144,7 @@ set(unit_sources
     test_tree.cpp
     test_transform.cpp
     test_uninitialized.cpp
+    test_unique.cpp
     test_unique_any.cpp
     test_variant.cpp
     test_vector.cpp
diff --git a/test/unit/test_cv_geom.cpp b/test/unit/test_cv_geom.cpp
index 0a0fdbaefa4f2df2f6ab6a94df9bb00f0ce4c192..63dca9639ff22219ac872fe958a3e787f12b19d7 100644
--- a/test/unit/test_cv_geom.cpp
+++ b/test/unit/test_cv_geom.cpp
@@ -11,16 +11,68 @@
 
 #include "common.hpp"
 #include "common_morphologies.hpp"
+#include "morph_pred.hpp"
 #include "../common_cells.hpp"
 
 using namespace arb;
 using util::make_span;
 
+::testing::AssertionResult verify_cv_children(const cv_geometry& g) {
+    unsigned visited_children = 0;
+    for (unsigned i = 0; i<g.size(); ++i) {
+        if (!util::is_sorted(g.children(i))) {
+            return ::testing::AssertionFailure() << "CV " << i
+                << " has unsorted sequence of child CVs";
+        }
+
+        for (auto cv: g.children(i)) {
+            if ((fvm_index_type)i != g.cv_parent.at(cv)) {
+                return ::testing::AssertionFailure() << "CV " << i
+                    << " has child CV " << cv << " which has parent " << g.cv_parent.at(cv);
+            }
+            ++visited_children;
+        }
+    }
+
+    if (g.cv_children.size()!=visited_children) {
+        return ::testing::AssertionFailure() << "geometry child CV count " << g.cv_children.size()
+            << " does not equal number of visited children " << visited_children;
+    }
+
+    unsigned n_nonempty_cells = 0;
+    for (auto c: util::make_span(g.n_cell())) {
+        if (!g.cell_cvs(c).empty()) {
+            ++n_nonempty_cells;
+        }
+    }
+    if (g.cv_children.size()!=g.size()-n_nonempty_cells) {
+        return ::testing::AssertionFailure() << "child CV count " << g.cv_children.size()
+            << " plus root CV count " << n_nonempty_cells
+            << " does not equal total number of CVs " << g.size();
+    }
+    return ::testing::AssertionSuccess();
+}
+
+namespace arb {
+namespace cv_prefer {
+std::ostream& operator<<(std::ostream& out, ::arb::cv_prefer::type p) {
+    switch (p) {
+    case cv_proximal: return out << "cv_proximal";
+    case cv_distal: return out << "cv_distal";
+    case cv_empty: return out << "cv_empty";
+    case cv_nonempty: return out << "cv_nonempty";
+    default: return out;
+    }
+}
+}
+}
+
 TEST(cv_geom, empty) {
     using namespace common_morphology;
 
     cable_cell empty_cell{m_empty};
     cv_geometry geom = cv_geometry_from_ends(empty_cell, ls::nil());
+    EXPECT_TRUE(verify_cv_children(geom));
 
     EXPECT_TRUE(geom.cv_parent.empty());
     EXPECT_TRUE(geom.cv_cables.empty());
@@ -44,11 +96,13 @@ TEST(cv_geom, trivial) {
         cable_cell cell{p.second};
         auto& m = cell.morphology();
 
-
         // Equivalent ways of specifying one CV comprising whole cell:
         cv_geometry geom1 = cv_geometry_from_ends(cell, ls::nil());
         cv_geometry geom2 = cv_geometry_from_ends(cell, ls::terminal());
 
+        EXPECT_TRUE(verify_cv_children(geom1));
+        EXPECT_TRUE(verify_cv_children(geom2));
+
         EXPECT_EQ(1u, geom1.size());
         EXPECT_EQ(geom1.cv_cables, geom2.cv_cables);
 
@@ -56,6 +110,9 @@ TEST(cv_geom, trivial) {
         cv_geometry geom3 = cv_geometry_from_ends(cell, ls::root());
         cv_geometry geom4 = cv_geometry_from_ends(cell, join(ls::root(), ls::terminal()));
 
+        EXPECT_TRUE(verify_cv_children(geom3));
+        EXPECT_TRUE(verify_cv_children(geom4));
+
         EXPECT_EQ(geom3.cv_cables, geom4.cv_cables);
         if (m.branch_children(mnpos).size()==1) {
             EXPECT_EQ(geom1.cv_cables, geom4.cv_cables);
@@ -76,7 +133,8 @@ TEST(cv_geom, one_cv_per_branch) {
         cable_cell cell{p.second};
         auto& m = cell.morphology();
 
-        cv_geometry geom = cv_geometry_from_ends(cell, ls::on_branches(0));
+        cv_geometry geom = cv_geometry_from_ends(cell, sum(ls::on_branches(0), ls::on_branches(1)));
+        EXPECT_TRUE(verify_cv_children(geom));
 
         // Expect trivial CVs at every fork point, and single-cable CVs for each branch.
         std::vector<unsigned> seen_branches(m.num_branches(), 0);
@@ -84,10 +142,10 @@ TEST(cv_geom, one_cv_per_branch) {
         for (auto i: make_span(geom.size())) {
             auto cables = geom.cables(i);
 
-            ASSERT_EQ(1u, cables.size());
             auto c = cables.front();
 
             if (c.prox_pos==c.dist_pos) {
+                EXPECT_LT(1u, cables.size());
                 if (c.branch==0 && c.prox_pos==0) {
                     EXPECT_TRUE(n_branch_child(mnpos)>1);
                 }
@@ -95,8 +153,11 @@ TEST(cv_geom, one_cv_per_branch) {
                     EXPECT_EQ(1., c.prox_pos);
                     EXPECT_TRUE(n_branch_child(c.branch)>1);
                 }
+                // Cables in trivial CV should be the same as those in the extent over the point.
+                EXPECT_TRUE(testing::seq_eq(mextent{m, mcable_list{c}}.cables(), cables));
             }
             else {
+                ASSERT_EQ(1u, cables.size());
                 ++seen_branches[c.branch];
                 EXPECT_EQ(1., seen_branches[c.branch]);
                 EXPECT_EQ(0., c.prox_pos);
@@ -104,21 +165,9 @@ TEST(cv_geom, one_cv_per_branch) {
 
                 // Confirm parent CV is fork CV:
                 if (i>0) {
-                    mlocation pfork = canonical(m, mlocation{c.branch, 0.});
-
-                    auto pcables = geom.cables(geom.cv_parent[i]);
-                    ASSERT_EQ(1u, pcables.size());
-
-                    mcable p = pcables.front();
-                    EXPECT_EQ(pfork.branch, p.branch);
-                    EXPECT_EQ(p.prox_pos, p.dist_pos);
-
-                    if (p.branch==0) {
-                        EXPECT_TRUE(p.prox_pos==0 || p.prox_pos==1);
-                    }
-                    else {
-                        EXPECT_EQ(1., p.prox_pos);
-                    }
+                    mextent fork_ext{m, mcable_list{{c.branch, 0}}};
+                    mcable_list pcables = util::assign_from(geom.cables(geom.cv_parent[i]));
+                    ASSERT_TRUE(testing::cablelist_eq(fork_ext.cables(), pcables));
                 }
             }
         }
@@ -139,6 +188,7 @@ TEST(cv_geom, midpoints) {
         auto& m = cell.morphology();
 
         cv_geometry geom = cv_geometry_from_ends(cell, ls::on_branches(0.5));
+        EXPECT_TRUE(verify_cv_children(geom));
 
         // Expect CVs to be either: covering fork points, with one cable per branch
         // at the fork (for a multiple-root-branch morphology, this would be treating
@@ -221,7 +271,8 @@ TEST(cv_geom, weird) {
     //        o
     //
     // CV 0 will comprise branches 0 and 2; CV 1 branches 1, 3, 5;
-    // and CV 2 branch 4.
+    // and CV 2 branch 4. CV 0 will also cover the fork point (0,1);
+    // CV 1 will cover the fork point (1, 1).
 
     using C = mcable;
     using testing::seq_eq;
@@ -229,12 +280,13 @@ TEST(cv_geom, weird) {
     cable_cell cell{common_morphology::m_reg_b6};
     cv_geometry geom = cv_geometry_from_ends(cell, mlocation_list{{1, 0}, {4,0}});
 
+    EXPECT_TRUE(verify_cv_children(geom));
     ASSERT_EQ(3u, geom.size());
 
-    mcable_list expected0 = {C{0u, 0., 1.}, C{2u, 0., 1.}};
+    mcable_list expected0 = {C{0u, 0., 1.}, C{1u, 0., 0.}, C{2u, 0., 1.}};
     EXPECT_TRUE(seq_eq(expected0, geom.cables(0)));
 
-    mcable_list expected1 = {C{1u, 0., 1.}, C{3u, 0., 1.}, C{5u, 0., 1.}};
+    mcable_list expected1 = {C{1u, 0., 1.}, C{3u, 0., 1.}, C{4u, 0., 0.}, C{5u, 0., 1.}};
     EXPECT_TRUE(seq_eq(expected1, geom.cables(1)));
 
     mcable_list expected2 = {C{4u, 0., 1.}};
@@ -245,48 +297,143 @@ TEST(cv_geom, location_cv) {
     using namespace common_morphology;
 
     cable_cell cell{m_reg_b6};
+    auto& m = cell.morphology();
+
+    auto cv_extent = [&m](const cv_geometry& geom, auto cv) {
+        mcable_list cl;
+        util::assign(cl, geom.cables(cv));
+        return mextent{m, cl};
+    };
 
     // Two CVs per branch, plus trivial CV at forks.
     cv_geometry geom = cv_geometry_from_ends(cell,
-       join(ls::on_branches(0.0), ls::on_branches(0.5), ls::on_branches(1.)));
+       join(ls::on_branches(0.), ls::on_branches(0.5), ls::on_branches(1.)));
 
-    // Confirm CVs are all only one cable, and either trivial or half a branch.
+    // Confirm CVs are either trivial or a single cable covering half a branch.
     for (auto cv: geom.cell_cvs(0)) {
         auto cables = geom.cables(cv);
-        ASSERT_EQ(1u, cables.size());
+        if (cables.size()==1u) {
+            // Half branch cable.
+            mcable cable = cables.front();
+            ASSERT_TRUE((cable.prox_pos==0 && cable.dist_pos==0.5 ) ||
+                        (cable.prox_pos==0.5 && cable.dist_pos==1.));
+        }
+        else {
+            // Trivial CV over fork point.
+            mcable cable0 = cables.front();
+            ASSERT_TRUE(cable0.prox_pos==cable0.dist_pos);
 
-        mcable cable = cables.front();
-        ASSERT_TRUE(cable.prox_pos==cable.dist_pos ||
-                    (cable.prox_pos==0 && cable.dist_pos==0.5 )||
-                    (cable.prox_pos==0.5 && cable.dist_pos==1.));
+            mextent fork_ext{m, mcable_list{cable0}};
+            mcable_list clist = util::assign_from(cables);
+            ASSERT_TRUE(testing::cablelist_eq(fork_ext.cables(), clist));
+        }
     }
 
-    // Where ambiguous, CV found should be one with a non-trivial cable containing the location.
-    for (auto bid: util::make_span(cell.morphology().num_branches())) {
-        for (double pos: {0., 0.3, 0.5, 0.7, 1.}) {
-            mlocation loc{bid, pos};
+    // For positions strictly within a CV extent, CV preference should make no difference.
+    for (auto prefer: {cv_prefer::cv_distal, cv_prefer::cv_proximal,
+                       cv_prefer::cv_nonempty, cv_prefer::cv_empty}) {
+        SCOPED_TRACE(prefer);
+        for (auto bid: util::make_span(m.num_branches())) {
+            for (double pos: {0.3, 0.7}) {
+                mlocation loc{bid, pos};
+                SCOPED_TRACE(loc);
+
+                auto cv = geom.location_cv(0, loc, prefer);
+                ASSERT_TRUE(cv_extent(geom, cv).intersects(loc));
+
+                ASSERT_EQ(1u, geom.cables(cv).size());
+                mcable cable = geom.cables(cv).front();
+                EXPECT_TRUE(cable.branch==loc.branch);
+                EXPECT_TRUE(cable.prox_pos<cable.dist_pos);
+            }
+        }
+    }
+
+    // For positions in the middle of a branch, we should get distal CV unless
+    // CV prerence is `cv_proximal`.
+    for (auto prefer: {cv_prefer::cv_distal, cv_prefer::cv_proximal,
+                       cv_prefer::cv_nonempty, cv_prefer::cv_empty}) {
+        SCOPED_TRACE(prefer);
+        for (auto bid: util::make_span(m.num_branches())) {
+            mlocation loc{bid, 0.5};
             SCOPED_TRACE(loc);
 
-            auto cv = geom.location_cv(0, loc);
+            auto cv = geom.location_cv(0, loc, prefer);
+            ASSERT_TRUE(cv_extent(geom, cv).intersects(loc));
 
+            ASSERT_EQ(1u, geom.cables(cv).size());
             mcable cable = geom.cables(cv).front();
-            EXPECT_TRUE(cable.prox_pos<cable.dist_pos);
-            EXPECT_TRUE(cable.prox_pos<=pos);
-            EXPECT_TRUE(cable.dist_pos>=pos);
             EXPECT_TRUE(cable.branch==loc.branch);
+            EXPECT_TRUE(cable.prox_pos<cable.dist_pos);
+
+            if (prefer==cv_prefer::cv_proximal) {
+                EXPECT_EQ(0., cable.prox_pos);
+            }
+            else {
+                EXPECT_EQ(0.5, cable.prox_pos);
+            }
         }
     }
 
-    // CV 0 will comprise branches 0 and 2; CV 1 branches 1, 3, 5;
-    // and CV 2 branch 4 (see test cv_geom.weird above).
-    cv_geometry gweird = cv_geometry_from_ends(cell, mlocation_list{{1, 0}, {4,0}});
-
-    // Expect location (4, 0.) to be in CV 2, but colocated locations
-    // (3, 0.), (5, 0.) and (1, 1.) to be in CV 1.
-    EXPECT_EQ(2u, gweird.location_cv(0, mlocation{4, 0.}));
-    EXPECT_EQ(1u, gweird.location_cv(0, mlocation{3, 0.}));
-    EXPECT_EQ(1u, gweird.location_cv(0, mlocation{5, 0.}));
-    EXPECT_EQ(1u, gweird.location_cv(0, mlocation{1, 1.}));
+    // For the head of a non-root branch, we should get the trivial CV over the
+    // fork for `cv_proximal` or `cv_empty`; otherwise the CV over the first
+    // half of the branch.
+    for (auto prefer: {cv_prefer::cv_distal, cv_prefer::cv_proximal,
+                       cv_prefer::cv_nonempty, cv_prefer::cv_empty}) {
+        SCOPED_TRACE(prefer);
+        for (auto bid: util::make_span(m.num_branches())) {
+            if (m.branch_parent(bid)==mnpos) continue;
+
+            mlocation loc{bid, 0.};
+            SCOPED_TRACE(loc);
+
+            auto cv = geom.location_cv(0, loc, prefer);
+            ASSERT_TRUE(cv_extent(geom, cv).intersects(loc));
+
+            auto cables = geom.cables(cv);
+            switch (prefer) {
+            case cv_prefer::cv_proximal:
+            case cv_prefer::cv_empty:
+                EXPECT_NE(1u, cables.size());
+                break;
+            case cv_prefer::cv_distal:
+            case cv_prefer::cv_nonempty:
+                EXPECT_EQ(1u, cables.size());
+                EXPECT_EQ(0.5, cables.front().dist_pos);
+                break;
+            }
+        }
+    }
+
+    // For the tail of a non-terminal branch, we should get the trivial CV over the
+    // fork for `cv_distal` or `cv_empty`; otherwise the CV over the second
+    // half of the branch.
+    for (auto prefer: {cv_prefer::cv_distal, cv_prefer::cv_proximal,
+                       cv_prefer::cv_nonempty, cv_prefer::cv_empty}) {
+        SCOPED_TRACE(prefer);
+        for (auto bid: util::make_span(m.num_branches())) {
+            if (m.branch_children(bid).empty()) continue;
+
+            mlocation loc{bid, 1.};
+            SCOPED_TRACE(loc);
+
+            auto cv = geom.location_cv(0, loc, prefer);
+            ASSERT_TRUE(cv_extent(geom, cv).intersects(loc));
+
+            auto cables = geom.cables(cv);
+            switch (prefer) {
+            case cv_prefer::cv_proximal:
+            case cv_prefer::cv_nonempty:
+                EXPECT_EQ(1u, cables.size());
+                EXPECT_EQ(0.5, cables.front().prox_pos);
+                break;
+            case cv_prefer::cv_distal:
+            case cv_prefer::cv_empty:
+                EXPECT_NE(1u, cables.size());
+                break;
+            }
+        }
+    }
 }
 
 TEST(cv_geom, multicell) {
@@ -301,6 +448,8 @@ TEST(cv_geom, multicell) {
     cv_geometry geom2 = geom;
     append(geom2, geom);
 
+    EXPECT_TRUE(verify_cv_children(geom));
+
     ASSERT_EQ(2*n_cv, geom2.size());
     for (unsigned i = 0; i<n_cv; ++i) {
         EXPECT_EQ(geom.cv_parent[i], geom2.cv_parent[i]);
diff --git a/test/unit/test_cv_layout.cpp b/test/unit/test_cv_layout.cpp
index 4d5f9710c8b51f1d98a1e40f86be6a4042777783..62989fd88b003265130f4a402fc1f42baabf4cfb 100644
--- a/test/unit/test_cv_layout.cpp
+++ b/test/unit/test_cv_layout.cpp
@@ -146,13 +146,14 @@ TEST(cv_layout, zero_size_cv) {
     // With one CV per branch, expect reference points for face conductance
     // to be at (0, 0.5); (0, 1); (1, 0.5); (2, 1); (3, 0.5); (4, 0.5); (5, 0.5).
     // The first CV should be all of branch 0; the second CV should be the
-    // zero-size CV at the branch point (0, 1).
+    // zero-size CV covering the branch point (0, 1).
     params.discretization = cv_policy_fixed_per_branch(1);
     fvm_cv_discretization D = fvm_cv_discretize(cell, params);
 
     unsigned cv_a = 0, cv_x = 1;
     ASSERT_TRUE(util::equal(mcable_list{mcable{0, 0, 1}}, D.geometry.cables(cv_a)));
-    ASSERT_TRUE(util::equal(mcable_list{mcable{0, 1, 1}}, D.geometry.cables(cv_x)));
+    ASSERT_TRUE(util::equal(mcable_list{mcable{0, 1, 1}, mcable{1, 0, 0}, mcable{2, 0, 0}},
+                    D.geometry.cables(cv_x)));
 
     // Find the two CV children of CV x.
     unsigned cv_b = -1, cv_c = -1;
diff --git a/test/unit/test_fvm_lowered.cpp b/test/unit/test_fvm_lowered.cpp
index 8957d1ff9c99458282ec7d820ce4cdef49ad6377..884d2df185a5c3fc8e720fe2b26c758e74e8bf89 100644
--- a/test/unit/test_fvm_lowered.cpp
+++ b/test/unit/test_fvm_lowered.cpp
@@ -1020,14 +1020,15 @@ TEST(fvm_lowered, gj_coords_complex) {
     fvcell.fvm_intdom(rec, gids, cell_to_intdom);
     fvm_cv_discretization D = fvm_cv_discretize(cells, neuron_parameter_defaults, context);
 
+    using namespace cv_prefer;
     int c0_gj_cv[2];
-    for (int i = 0; i<2; ++i) c0_gj_cv[i] = D.geometry.location_cv(0, c0_gj[i]);
+    for (int i = 0; i<2; ++i) c0_gj_cv[i] = D.geometry.location_cv(0, c0_gj[i], cv_nonempty);
 
     int c1_gj_cv[4];
-    for (int i = 0; i<4; ++i) c1_gj_cv[i] = D.geometry.location_cv(1, c1_gj[i]);
+    for (int i = 0; i<4; ++i) c1_gj_cv[i] = D.geometry.location_cv(1, c1_gj[i], cv_nonempty);
 
     int c2_gj_cv[3];
-    for (int i = 0; i<3; ++i) c2_gj_cv[i] = D.geometry.location_cv(2, c2_gj[i]);
+    for (int i = 0; i<3; ++i) c2_gj_cv[i] = D.geometry.location_cv(2, c2_gj[i], cv_nonempty);
 
     std::vector<fvm_gap_junction> GJ = fvcell.fvm_gap_junctions(cells, gids, rec, D);
     EXPECT_EQ(10u, GJ.size());
diff --git a/test/unit/test_unique.cpp b/test/unit/test_unique.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..15e49ea1c690e9ecceefb823aeaf52d9e131e04b
--- /dev/null
+++ b/test/unit/test_unique.cpp
@@ -0,0 +1,39 @@
+#include "../gtest.h"
+
+#include <vector>
+#include <list>
+#include <utility>
+
+#include "util/unique.hpp"
+#include "./common.hpp"
+
+namespace {
+auto same_parity = [](auto a, auto b) { return a%2 == b%2; };
+
+template <typename C, typename Eq = std::equal_to<>>
+void run_unique_in_place_test(C data, const C& expected, Eq eq = Eq{}) {
+    arb::util::unique_in_place(data, eq);
+    EXPECT_TRUE(testing::seq_eq(data, expected));
+}
+
+template <typename C>
+void run_tests() {
+    run_unique_in_place_test(C{}, C{});
+    run_unique_in_place_test(C{1, 3, 2}, C{1, 3, 2});
+    run_unique_in_place_test(C{1, 1, 1, 3, 2}, C{1, 3, 2});
+    run_unique_in_place_test(C{1, 3, 3, 3, 2}, C{1, 3, 2});
+    run_unique_in_place_test(C{1, 3, 2, 2, 2}, C{1, 3, 2});
+    run_unique_in_place_test(C{1, 1, 3, 2, 2}, C{1, 3, 2});
+    run_unique_in_place_test(C{3, 1, 3, 1, 1, 3, 1, 1}, C{3, 1, 3, 1, 3, 1});
+    run_unique_in_place_test(C{1, 2, 4, 1, 3, 1, 2, 1}, C{1, 2, 1, 2, 1}, same_parity);
+}
+}
+
+TEST(unique_in_place, vector) {
+    run_tests<std::vector<int>>();
+}
+
+TEST(unique_in_place, list) {
+    run_tests<std::list<int>>();
+}
+