diff --git a/arbor/CMakeLists.txt b/arbor/CMakeLists.txt
index e7d9740df549268667da5a7c4b21c5fcf6b9c12a..c8e7c339d6ad689183977fa2e6660fdcce7afd82 100644
--- a/arbor/CMakeLists.txt
+++ b/arbor/CMakeLists.txt
@@ -28,9 +28,14 @@ set(arbor_sources
     mechcat.cpp
     memory/cuda_wrappers.cpp
     memory/util.cpp
+    morph/em_morphology.cpp
+    morph/label_dict.cpp
+    morph/locset.cpp
+    morph/mbranch.cpp
     morph/morphology.cpp
-    morph/sample_tree.cpp
     morph/primitives.cpp
+    morph/region.cpp
+    morph/sample_tree.cpp
     merge_events.cpp
     simulation.cpp
     partition_load_balance.cpp
diff --git a/arbor/cable_cell.cpp b/arbor/cable_cell.cpp
index 50ca10c05c968e6200e51d7823b402c618797608..90dcd74438148e6adf4d9cd262af4ea004303a21 100644
--- a/arbor/cable_cell.cpp
+++ b/arbor/cable_cell.cpp
@@ -1,9 +1,15 @@
+#include <sstream>
+#include <vector>
+
 #include <arbor/cable_cell.hpp>
+#include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/segment.hpp>
 
+#include "morph/em_morphology.hpp"
 #include "util/rangeutil.hpp"
 #include "util/span.hpp"
+#include "util/strprintf.hpp"
 
 namespace arb {
 
@@ -72,6 +78,17 @@ bool cable_cell::has_soma() const {
     return !segment(0)->is_placeholder();
 }
 
+void cable_cell::paint(const std::string& target, const std::string& description) {
+    auto it = regions_.find(target);
+
+    // Nothing to do if there are no regions that match.
+    if (it==regions_.end()) return;
+
+    for (auto i: it->second) {
+        segment(i)->add_mechanism(description);
+    }
+}
+
 soma_segment* cable_cell::soma() {
     return has_soma()? segment(0)->as_soma(): nullptr;
 }
@@ -106,12 +123,12 @@ size_type cable_cell::num_compartments() const {
             [](const segment_ptr& s) { return s->num_compartments(); });
 }
 
-void cable_cell::add_stimulus(segment_location loc, i_clamp stim) {
-    (void)segment(loc.segment); // assert loc.segment in range
+void cable_cell::add_stimulus(mlocation loc, i_clamp stim) {
+    (void)segment(loc.branch); // assert loc.segment in range
     stimuli_.push_back({loc, std::move(stim)});
 }
 
-void cable_cell::add_detector(segment_location loc, double threshold) {
+void cable_cell::add_detector(mlocation loc, double threshold) {
     spike_detectors_.push_back({loc, threshold});
 }
 
@@ -164,24 +181,27 @@ value_type cable_cell::segment_mean_attenuation(
     return 2*std::sqrt(math::pi<double>*tau_per_um*frequency)*length_factor; // [1/µm]
 }
 
-cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discretization) {
+cable_cell make_cable_cell(const morphology& m,
+                           const label_dict& dictionary,
+                           bool compartments_from_discretization)
+{
     using point3d = cable_cell::point_type;
     cable_cell newcell;
 
-    if (!morph.num_branches()) {
+    if (!m.num_branches()) {
         return newcell;
     }
 
     // Add the soma.
-    auto loc = morph.samples()[0].loc; // location of soma.
+    auto loc = m.samples()[0].loc; // location of soma.
 
     // If there is no spherical root/soma use a zero-radius soma.
-    double srad = morph.spherical_root()? loc.radius: 0.;
+    double srad = m.spherical_root()? loc.radius: 0.;
     newcell.add_soma(srad, point3d(loc.x, loc.y, loc.z));
 
-    auto& samples = morph.samples();
-    for (auto i: util::make_span(1, morph.num_branches())) {
-        auto index =  util::make_range(morph.branch_sample_span(i));
+    auto& samples = m.samples();
+    for (auto i: util::make_span(1, m.num_branches())) {
+        auto index =  util::make_range(m.branch_indexes(i));
 
         // find kind for the branch. Use the tag of the last sample in the branch.
         int tag = samples[index.back()].tag;
@@ -207,9 +227,9 @@ cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discr
         }
 
         // Find the id of this branch's parent.
-        auto pid = morph.branch_parent(i);
+        auto pid = m.branch_parent(i);
         // Adjust pid if a zero-radius soma was used.
-        if (!morph.spherical_root()) {
+        if (!m.spherical_root()) {
             pid = pid==mnpos? 0: pid+1;
         }
         auto cable = newcell.add_cable(pid, kind, radii, points);
@@ -218,6 +238,26 @@ cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discr
         }
     }
 
+    // Construct concrete regions.
+    // Ignores the pointsets, for now.
+    auto em = em_morphology(m); // for converting labels to "segments"
+
+    std::unordered_map<std::string, std::vector<msize_t>> regions;
+    for (auto r: dictionary.regions()) {
+        mcable_list L = thingify(r.second, em);
+        std::vector<msize_t> bids;
+        for (auto c: L) {
+            if (c.prox_pos!=0 || c.dist_pos!=1) {
+                throw cable_cell_error(util::pprintf(
+                    "cable_cell does not support regions with partial branches: \"{}\": {}",
+                    r.first, c));
+            }
+            bids.push_back(c.branch);
+        }
+        regions[r.first] = bids;
+    }
+    newcell.set_regions(std::move(regions));
+
     return newcell;
 }
 
diff --git a/arbor/fvm_layout.cpp b/arbor/fvm_layout.cpp
index f035b70b88f8b2290a281a8af1f5a3ce4d08d8c8..aa0e9b3af846fcc9a2b0011432d7d3ab68c7eeb9 100644
--- a/arbor/fvm_layout.cpp
+++ b/arbor/fvm_layout.cpp
@@ -612,15 +612,15 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
 
         for (const auto& cellsyn: cell.synapses()) {
             const mechanism_desc& desc = cellsyn.mechanism;
-            size_type cv = D.segment_location_cv(cell_idx, cellsyn.location);
+            size_type cv = D.branch_location_cv(cell_idx, cellsyn.location);
             const auto& name = desc.name();
 
             point_mech_data& entry = point_mech_table[name];
             update_paramset_and_validate(desc, entry.info, entry.paramset);
             entry.points.push_back({cv, target_id++, &desc});
 
-            const segment_ptr& seg = cell.segments()[cellsyn.location.segment];
-            size_type segment_idx = D.cell_segment_bounds[cell_idx]+cellsyn.location.segment;
+            const segment_ptr& seg = cell.segments()[cellsyn.location.branch];
+            size_type segment_idx = D.cell_segment_bounds[cell_idx]+cellsyn.location.branch;
 
             for (const auto& ion_entry: entry.info->ions) {
                 const std::string& ion_name = ion_entry.first;
@@ -635,7 +635,7 @@ fvm_mechanism_data fvm_build_mechanism_data(const cable_cell_global_properties&
         }
 
         for (const auto& stimulus: cell.stimuli()) {
-            size_type cv = D.segment_location_cv(cell_idx, stimulus.location);
+            size_type cv = D.branch_location_cv(cell_idx, stimulus.location);
             stimuli.push_back({cv, stimulus.clamp});
         }
 
diff --git a/arbor/fvm_layout.hpp b/arbor/fvm_layout.hpp
index c0172e06e21ba10a15cba1c117a6bcc1d6298b70..879be39b806e9e2cf455655eb988d1087a0786c6 100644
--- a/arbor/fvm_layout.hpp
+++ b/arbor/fvm_layout.hpp
@@ -85,12 +85,12 @@ struct fvm_discretization {
         return util::partition_view(cell_cv_bounds);
     }
 
-    size_type segment_location_cv(size_type cell_index, segment_location segloc) const {
+    size_type branch_location_cv(size_type cell_index, mlocation loc) const {
         auto cell_segs = cell_segment_part()[cell_index];
 
-        size_type seg = segloc.segment+cell_segs.first;
+        size_type seg = loc.branch+cell_segs.first;
         arb_assert(seg<cell_segs.second);
-        return segments[seg].cv_by_position(segloc.position);
+        return segments[seg].cv_by_position(loc.pos);
     }
 };
 
diff --git a/arbor/fvm_lowered_cell_impl.hpp b/arbor/fvm_lowered_cell_impl.hpp
index a466077b19611dcc2132e511a8c49c1ab7f30290..b016793e68aa2c559161d1170a5fd7f2c468e0df 100644
--- a/arbor/fvm_lowered_cell_impl.hpp
+++ b/arbor/fvm_lowered_cell_impl.hpp
@@ -507,7 +507,7 @@ void fvm_lowered_cell_impl<B>::initialize(
         cell_gid_type gid = gids[cell_idx];
 
         for (auto detector: cells[cell_idx].detectors()) {
-            detector_cv.push_back(D.segment_location_cv(cell_idx, detector.location));
+            detector_cv.push_back(D.branch_location_cv(cell_idx, detector.location));
             detector_threshold.push_back(detector.threshold);
         }
 
@@ -515,7 +515,7 @@ 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.segment_location_cv(cell_idx, where.location);
+            auto cv = D.branch_location_cv(cell_idx, where.location);
             probe_handle handle;
 
             switch (where.kind) {
@@ -555,7 +555,7 @@ std::vector<fvm_gap_junction> fvm_lowered_cell_impl<B>::fvm_gap_junctions(
 
             auto cell_gj = cells[cell_idx].gap_junction_sites();
             for (auto gj : cell_gj) {
-                auto cv = D.segment_location_cv(cell_idx, gj);
+                auto cv = D.branch_location_cv(cell_idx, gj);
                 gid_to_cvs[gids[cell_idx]].push_back(cv);
             }
         }
diff --git a/arbor/include/arbor/cable_cell.hpp b/arbor/include/arbor/cable_cell.hpp
index 09baa5de0507dc6a8e28f51e3f59112892b45ba0..2303aaff7f63e70277b005b0c38e7fd1f9c307ea 100644
--- a/arbor/include/arbor/cable_cell.hpp
+++ b/arbor/include/arbor/cable_cell.hpp
@@ -9,28 +9,13 @@
 #include <arbor/common_types.hpp>
 #include <arbor/constants.hpp>
 #include <arbor/mechcat.hpp>
+#include <arbor/morph/label_dict.hpp>
 #include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
 #include <arbor/segment.hpp>
 
 namespace arb {
 
-// Location specification for point processes.
-
-struct segment_location {
-    segment_location(cell_lid_type s, double l):
-        segment(s), position(l)
-    {
-        arb_assert(position>=0. && position<=1.);
-    }
-
-     bool operator==(segment_location other) const {
-        return segment==other.segment && position==other.position;
-    }
-
-    cell_lid_type segment;
-    double position;
-};
-
 // Current clamp description for stimulus specification.
 
 struct i_clamp {
@@ -52,7 +37,7 @@ struct cell_probe_address {
         membrane_voltage, membrane_current
     };
 
-    segment_location location;
+    mlocation location;
     probe_kind kind;
 };
 
@@ -64,20 +49,22 @@ public:
     using value_type = double;
     using point_type = point<value_type>;
 
-    using gap_junction_instance = segment_location;
+    using gap_junction_instance = mlocation;
+
+    using region_map = std::unordered_map<std::string, std::vector<msize_t>>;
 
     struct synapse_instance {
-        segment_location location;
+        mlocation location;
         mechanism_desc mechanism;
     };
 
     struct stimulus_instance {
-        segment_location location;
+        mlocation location;
         i_clamp clamp;
     };
 
     struct detector_instance {
-        segment_location location;
+        mlocation location;
         double threshold;
     };
 
@@ -93,7 +80,8 @@ public:
         stimuli_(other.stimuli_),
         synapses_(other.synapses_),
         gap_junction_sites_(other.gap_junction_sites_),
-        spike_detectors_(other.spike_detectors_)
+        spike_detectors_(other.spike_detectors_),
+        regions_(other.regions_)
     {
         // unique_ptr's cannot be copy constructed, do a manual assignment
         segments_.reserve(other.segments_.size());
@@ -158,7 +146,7 @@ public:
     //////////////////
     // stimuli
     //////////////////
-    void add_stimulus(segment_location loc, i_clamp stim);
+    void add_stimulus(mlocation loc, i_clamp stim);
 
     std::vector<stimulus_instance>&
     stimuli() {
@@ -170,10 +158,15 @@ public:
         return stimuli_;
     }
 
+    //////////////////
+    // painters
+    //////////////////
+    void paint(const std::string& target, const std::string& description);
+
     //////////////////
     // synapses
     //////////////////
-    void add_synapse(segment_location loc, mechanism_desc p)
+    void add_synapse(mlocation loc, mechanism_desc p)
     {
         synapses_.push_back(synapse_instance{loc, std::move(p)});
     }
@@ -184,7 +177,7 @@ public:
     //////////////////
     // gap-junction
     //////////////////
-    void add_gap_junction(segment_location location)
+    void add_gap_junction(mlocation location)
     {
         gap_junction_sites_.push_back(location);
     }
@@ -195,7 +188,7 @@ public:
     //////////////////
     // spike detectors
     //////////////////
-    void add_detector(segment_location loc, double threshold);
+    void add_detector(mlocation loc, double threshold);
 
     std::vector<detector_instance>&
     detectors() {
@@ -207,6 +200,10 @@ public:
         return spike_detectors_;
     }
 
+    void set_regions(region_map r) {
+        regions_ = std::move(r);
+    }
+
     // Checks that two cells have the same
     //  - number and type of segments
     //  - volume and area properties of each segment
@@ -253,6 +250,9 @@ private:
 
     // the sensors
     std::vector<detector_instance> spike_detectors_;
+
+    // Named regions, oh my.
+    region_map regions_;
 };
 
 // create a cable by forwarding cable construction parameters provided by the user
@@ -273,6 +273,8 @@ cable_segment* cable_cell::add_cable(cable_cell::index_type parent, Args&&... ar
 // If compartments_from_discretization is true, set number of compartments
 // in each segment to be the number of piecewise linear sections in the
 // corresponding section of the morphology.
-cable_cell make_cable_cell(const morphology& morph, bool compartments_from_discretization);
+cable_cell make_cable_cell(const morphology& morph,
+                           const label_dict& labels={},
+                           bool compartments_from_discretization=false);
 
 } // namespace arb
diff --git a/arbor/include/arbor/morph/label_dict.hpp b/arbor/include/arbor/morph/label_dict.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..852cef7121fb4d1a3a6b69b3bb1a71d7e1287a50
--- /dev/null
+++ b/arbor/include/arbor/morph/label_dict.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <memory>
+#include <unordered_map>
+
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/region.hpp>
+#include <arbor/util/optional.hpp>
+
+namespace arb {
+
+class label_dict {
+    using ps_map = std::unordered_map<std::string, arb::locset>;
+    using reg_map = std::unordered_map<std::string, arb::region>;
+    ps_map locsets_;
+    reg_map regions_;
+
+public:
+
+    void set(const std::string& name, locset ls);
+    void set(const std::string& name, region reg);
+
+    util::optional<const arb::region&> region(const std::string& name) const;
+    util::optional<const arb::locset&> locset(const std::string& name) const;
+
+    const ps_map& locsets() const;
+    const reg_map& regions() const;
+
+    std::size_t size() const;
+};
+
+} //namespace arb
diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..00c7e97386025aa9dbbeea782bb0ed178e01733a
--- /dev/null
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -0,0 +1,122 @@
+#pragma once
+
+#include <cstdlib>
+#include <memory>
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/morphology.hpp>
+
+namespace arb {
+
+// Forward declare the backend em_morphology type, required for defining the
+// interface for concretising locsets.
+class em_morphology;
+
+class locset {
+public:
+    locset() = delete;
+
+    template <typename Impl,
+              typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, locset>::value>>
+    explicit locset(Impl&& impl):
+        impl_(new wrap<Impl>(std::forward<Impl>(impl))) {}
+
+    template <typename Impl>
+    explicit locset(const Impl& impl):
+        impl_(new wrap<Impl>(impl)) {}
+
+    locset(locset&& other) = default;
+
+    locset(const locset& other):
+        impl_(other.impl_->clone()) {}
+
+    locset& operator=(const locset& other) {
+        impl_ = other.impl_->clone();
+        return *this;
+    }
+
+    template <typename Impl,
+              typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, locset>::value>>
+    locset& operator=(Impl&& other) {
+        impl_ = new wrap<Impl>(std::forward<Impl>(other));
+        return *this;
+    }
+
+    template <typename Impl>
+    locset& operator=(const Impl& other) {
+        impl_ = new wrap<Impl>(other);
+        return *this;
+    }
+
+    friend mlocation_list thingify(const locset& p, const em_morphology& m) {
+        return p.impl_->thingify(m);
+    }
+
+    friend std::ostream& operator<<(std::ostream& o, const locset& p) {
+        return p.impl_->print(o);
+    }
+
+    // The sum of two location sets.
+    friend locset sum(locset, locset);
+
+    template <typename ...Args>
+    friend locset sum(locset l, locset r, Args... args) {
+        return sum(sum(std::move(l), std::move(r)), std::move(args)...);
+    }
+
+private:
+    struct interface {
+        virtual ~interface() {}
+        virtual std::unique_ptr<interface> clone() = 0;
+        virtual std::ostream& print(std::ostream&) = 0;
+        virtual mlocation_list thingify(const em_morphology&) = 0;
+    };
+
+    std::unique_ptr<interface> impl_;
+
+    template <typename Impl>
+    struct wrap: interface {
+        explicit wrap(const Impl& impl): wrapped(impl) {}
+        explicit wrap(Impl&& impl): wrapped(std::move(impl)) {}
+
+        virtual std::unique_ptr<interface> clone() override {
+            return std::unique_ptr<interface>(new wrap<Impl>(wrapped));
+        }
+
+        virtual mlocation_list thingify(const em_morphology& m) override {
+            return thingify_(wrapped, m);
+        }
+
+        virtual std::ostream& print(std::ostream& o) override {
+            return o << wrapped;
+        }
+
+        Impl wrapped;
+    };
+};
+
+namespace ls {
+
+// Location of a sample.
+locset location(mlocation);
+
+// Location of a sample.
+locset sample(msize_t);
+
+// Set of terminal nodes on a morphology.
+locset terminal();
+
+// The root node of a morphology.
+locset root();
+
+// The null (empty) set.
+locset nil();
+
+} // namespace ls
+
+} // namespace arb
diff --git a/arbor/include/arbor/morph/morphology.hpp b/arbor/include/arbor/morph/morphology.hpp
index 4ad07955f68af5ea976bdb8d89ecdf74fbffa280..6eed5ef9abc081b61961dc7fc1f849c3e650f30b 100644
--- a/arbor/include/arbor/morph/morphology.hpp
+++ b/arbor/include/arbor/morph/morphology.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <memory>
 #include <ostream>
 #include <vector>
 
@@ -9,38 +10,13 @@
 
 namespace arb {
 
-// An unbranched cable segment that has root, terminal or fork point at each end.
-struct mbranch {
-    std::vector<msize_t> index;  // sample index
-    msize_t parent_id = mnpos;   // branch index
+class morphology_impl;
 
-    mbranch() = default;
-    mbranch(std::vector<msize_t> idx, msize_t parent):
-        index(std::move(idx)), parent_id(parent) {}
-
-    bool is_sphere()  const { return size()==1u; }
-    msize_t size()    const { return index.size(); }
-    bool has_parent() const { return parent_id!=mnpos;}
-
-    friend bool operator==(const mbranch& l, const mbranch& r);
-    friend std::ostream& operator<<(std::ostream& o, const mbranch& b);
-};
+using mindex_range = std::pair<const msize_t*, const msize_t*>;
 
 class morphology {
-    // The sample tree of sample points and their parent-child relationships.
-    sample_tree samples_;
-
-    // Indicates whether the soma is a sphere.
-    bool spherical_root_ = false;
-
-    // Branch state.
-    std::vector<mbranch> branches_;
-    std::vector<msize_t> branch_parents_;
-    std::vector<std::vector<msize_t>> branch_children_;
-
-    using index_range = std::pair<const msize_t*, const msize_t*>;
-
-    void init();
+    // Hold an immutable copy of the morphology implementation.
+    std::shared_ptr<const morphology_impl> impl_;
 
 public:
     morphology(sample_tree m, bool use_spherical_root);
@@ -52,8 +28,8 @@ public:
     // The number of branches in the morphology.
     msize_t num_branches() const;
 
-    // The parent sample of sample i.
-    const std::vector<msize_t>& sample_parents() const;
+    // The number of samples in the morphology.
+    msize_t num_samples() const;
 
     // The parent branch of branch b.
     msize_t branch_parent(msize_t b) const;
@@ -62,11 +38,17 @@ public:
     const std::vector<msize_t>& branch_children(msize_t b) const;
 
     // Range of indexes into the sample points in branch b.
-    index_range branch_sample_span(msize_t b) const;
+    mindex_range branch_indexes(msize_t b) const;
 
-    // Range of the samples in branch b.
+    // All of the samples in the morphology.
     const std::vector<msample>& samples() const;
 
+    // The parent sample of sample i.
+    const std::vector<msize_t>& sample_parents() const;
+
+    // Point properties of samples in the morphology.
+    const std::vector<point_prop>& sample_props() const;
+
     friend std::ostream& operator<<(std::ostream&, const morphology&);
 };
 
diff --git a/arbor/include/arbor/morph/primitives.hpp b/arbor/include/arbor/morph/primitives.hpp
index 42baa9b7f46c6d24d28df259fd9478cfb8daf60b..ae58c511f7eb2cb0afd076b01a3c5f5c7e1eb92a 100644
--- a/arbor/include/arbor/morph/primitives.hpp
+++ b/arbor/include/arbor/morph/primitives.hpp
@@ -48,6 +48,53 @@ struct msample {
 bool is_collocated(const msample& a, const msample& b);
 double distance(const msample& a, const msample& b);
 
+// Describe a specific location on a morpholology.
+struct mlocation {
+    // The id of the branch.
+    msize_t branch;
+    // The relative position on the branch ∈ [0,1].
+    double pos;
+
+    // branch ≠ npos and 0 ≤ pos ≤ 1
+    friend bool test_invariants(const mlocation&);
+    friend std::ostream& operator<<(std::ostream&, const mlocation&);
+};
+
+ARB_DEFINE_LEXICOGRAPHIC_ORDERING(mlocation, (a.branch,a.pos), (b.branch,b.pos));
+
+using mlocation_list = std::vector<mlocation>;
+std::ostream& operator<<(std::ostream& o, const mlocation_list& l);
+
+// Tests whether each location in the list satisfies the invariants for a location,
+// and that the locations in the vector are ordered.
+bool test_invariants(const mlocation_list&);
+
+// Describe an unbranched cable in the morphology.
+struct mcable {
+    // The id of the branch on which the cable lies.
+    msize_t branch;
+
+    // Relative location of the end points on the branch.
+    // 0 ≤ prox_pos ≤ dist_pos ≤ 1
+    double prox_pos; // ∈ [0,1]
+    double dist_pos; // ∈ [0,1]
+
+    friend mlocation prox_loc(const mcable&);
+    friend mlocation dist_loc(const mcable&);
+
+    // branch ≠ npos, and 0 ≤ prox_pos ≤ dist_pos ≤ 1
+    friend bool test_invariants(const mcable&);
+    friend std::ostream& operator<<(std::ostream&, const mcable&);
+};
+
+ARB_DEFINE_LEXICOGRAPHIC_ORDERING(mcable, (a.branch,a.prox_pos,a.dist_pos), (b.branch,b.prox_pos,b.dist_pos));
+
+using mcable_list = std::vector<mcable>;
+std::ostream& operator<<(std::ostream& o, const mcable_list& c);
+// Tests whether each cable in the list satisfies the invariants for a cable,
+// and that the cables in the vector are ordered.
+bool test_invariants(const mcable_list&);
+
 using point_prop = std::uint8_t;
 enum point_prop_mask: point_prop {
     point_prop_mask_none = 0,
diff --git a/arbor/include/arbor/morph/region.hpp b/arbor/include/arbor/morph/region.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d3d7f735d553202da1070300be948a57619bed47
--- /dev/null
+++ b/arbor/include/arbor/morph/region.hpp
@@ -0,0 +1,129 @@
+#pragma once
+
+#include <cstdlib>
+#include <memory>
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/morphology.hpp>
+
+namespace arb {
+
+// Forward declare the backend em_morphology type, required for defining the
+// interface for concretising locsets.
+class em_morphology;
+
+class region {
+public:
+    region() = delete;
+
+    template <typename Impl,
+              typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, region>::value>>
+    explicit region(Impl&& impl):
+        impl_(new wrap<Impl>(std::forward<Impl>(impl))) {}
+
+    template <typename Impl>
+    explicit region(const Impl& impl):
+        impl_(new wrap<Impl>(impl)) {}
+
+    region(region&& other) = default;
+
+    region(const region& other):
+        impl_(other.impl_->clone()) {}
+
+    region& operator=(const region& other) {
+        impl_ = other.impl_->clone();
+        return *this;
+    }
+
+    template <typename Impl,
+              typename X=std::enable_if_t<!std::is_same<std::decay_t<Impl>, region>::value>>
+    region& operator=(Impl&& other) {
+        impl_ = new wrap<Impl>(std::forward<Impl>(other));
+        return *this;
+    }
+
+    template <typename Impl>
+    region& operator=(const Impl& other) {
+        impl_ = new wrap<Impl>(other);
+        return *this;
+    }
+
+    friend mcable_list thingify(const region& r, const em_morphology& m) {
+        return r.impl_->thingify(m);
+    }
+
+    friend std::ostream& operator<<(std::ostream& o, const region& p) {
+        return p.impl_->print(o);
+    }
+
+    // The union of regions.
+    friend region join(region, region);
+
+    template <typename ...Args>
+    friend region join(region l, region r, Args... args) {
+        return join(join(std::move(l), std::move(r)), std::move(args)...);
+    }
+
+    // The intersection of regions.
+    friend region intersect(region, region);
+
+    template <typename ...Args>
+    friend region intersect(region l, region r, Args... args) {
+        return intersect(intersect(std::move(l), std::move(r)), std::move(args)...);
+    }
+
+private:
+    struct interface {
+        virtual ~interface() {}
+        virtual std::unique_ptr<interface> clone() = 0;
+        virtual std::ostream& print(std::ostream&) = 0;
+        virtual mcable_list thingify(const em_morphology&) = 0;
+    };
+
+    std::unique_ptr<interface> impl_;
+
+    template <typename Impl>
+    struct wrap: interface {
+        explicit wrap(const Impl& impl): wrapped(impl) {}
+        explicit wrap(Impl&& impl): wrapped(std::move(impl)) {}
+
+        virtual std::unique_ptr<interface> clone() override {
+            return std::unique_ptr<interface>(new wrap<Impl>(wrapped));
+        }
+
+        virtual mcable_list thingify(const em_morphology& m) override {
+            return thingify_(wrapped, m);
+        }
+
+        virtual std::ostream& print(std::ostream& o) override {
+            return o << wrapped;
+        }
+
+        Impl wrapped;
+    };
+};
+
+namespace reg {
+
+// An explicit cable section.
+region cable(mcable);
+
+region interval(mlocation, mlocation);
+
+// An explicit branch.
+region branch(msize_t);
+
+// Region with all segments with segment tag id.
+region tagged(int id);
+
+// Region with all segments in a cell.
+region all();
+
+} // namespace reg
+
+} // namespace arb
diff --git a/arbor/morph/em_morphology.cpp b/arbor/morph/em_morphology.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6e482c7e062e68eefc4545e0129b7441e1aa7042
--- /dev/null
+++ b/arbor/morph/em_morphology.cpp
@@ -0,0 +1,144 @@
+#include <mutex>
+#include <vector>
+
+#include <arbor/morph/error.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+
+#include "morph/em_morphology.hpp"
+#include "util/rangeutil.hpp"
+#include "util/span.hpp"
+#include "util/strprintf.hpp"
+
+namespace arb {
+
+em_morphology::em_morphology(const morphology& m):
+    morph_(m)
+{
+    using util::count_along;
+    using util::make_span;
+
+    auto& parents = morph_.sample_parents();
+    auto& samples = morph_.samples();
+
+    const auto ns = morph_.num_samples();
+    const auto nb = morph_.num_branches();
+
+    // Cache distance of each sample from the root.
+    dist2root_.resize(ns);
+    dist2root_[0] = 0.;
+    for (auto i: make_span(1, ns)) {
+        const auto p = parents[i];
+        dist2root_[i] = dist2root_[p] + distance(samples[p], samples[i]);
+    }
+    // Cache the legth of each branch.
+    branch_lengths_.reserve(nb);
+    for (auto i: make_span(nb)) {
+        auto idx = util::make_range(morph_.branch_indexes(i));
+        branch_lengths_.push_back(dist2root_[idx.back()]- dist2root_[idx.front()]);
+    }
+
+    // Cache the sample locations.
+    // Iterate backwards over branches distal to root, so that the parent branch at
+    // fork points will label its distal sample.
+    sample_locs_.resize(ns);
+    for (int b=nb-1; b>=0; --b) {
+        auto idx = util::make_range(morph_.branch_indexes(b));
+        double len = branch_lengths_[b];
+        // Handle 0 length branch.
+        len = len==0.? 1.: len;
+        double start = dist2root_[idx.front()];
+        for (auto i: idx) {
+            sample_locs_[i] = {msize_t(b), (dist2root_[i]-start)/len};
+        }
+        // For ensure that all non-spherical branches have their last sample 
+        if (idx.size()>1u) {
+            sample_locs_[idx.back()] = mlocation{msize_t(b), 1};
+        }
+    }
+    sample_locs_[0] = mlocation{0, 0.};
+
+    // Cache the location of terminal and fork points.
+    auto& props = morph_.sample_props();
+    for (auto i: count_along(props)) {
+        auto p = props[i];
+        if (is_terminal(p)) {
+            terminals_.push_back(sample2loc(i));
+        }
+        if (is_fork(p)) {
+            forks_.push_back(sample2loc(i));
+        }
+    }
+}
+
+const morphology& em_morphology::morph() const {
+    return morph_;
+}
+
+mlocation em_morphology::root() const {
+    return {0,0};
+}
+
+mlocation em_morphology::sample2loc(msize_t sid) const {
+    if (sid>=morph_.num_samples()) {
+        throw morphology_error(util::pprintf("Sample {} does not exist in morpology", sid));
+    }
+    return sample_locs_[sid];
+}
+
+mlocation_list em_morphology::terminals() const {
+    return terminals_;
+}
+
+mlocation_list em_morphology::cover(mlocation loc, bool include_loc) const {
+    mlocation_list L{};
+    if (include_loc) L.push_back(loc);
+
+    // If the location is not at the end of a branch, it is its own cover.
+    if (loc.pos>0. && loc.pos<1.) return L;
+
+    // First location is {0,0} on a spherical root: nothing more to do.
+    if (loc==mlocation{0,0} && morph_.spherical_root()) {
+        return L;
+    }
+
+    if (loc.pos==1) {
+        // The location is at the end of a branch:
+        //      add the location at the start of each child branch.
+        for (auto b: morph_.branch_children(loc.branch)) {
+            L.push_back({b, 0});
+        }
+    }
+    else if (loc.pos==0) {
+        // The location is at the start of a branch:
+        //      add the location at the end of the parent branch, and locations
+        //      at the start of each child of the parent branch.
+        auto p = morph_.branch_parent(loc.branch);
+        if (p!=mnpos) L.push_back({p, 1});
+        for (auto b: morph_.branch_children(p)) {
+            if (b!=loc.branch) L.push_back({b, 0});
+        }
+    }
+
+    util::sort(L);
+
+    return L;
+}
+
+mlocation em_morphology::canonicalize(mlocation loc) const {
+    if (!test_invariants(loc)) {
+        throw morphology_error(util::pprintf("Invalid location {}", loc));
+    }
+    if (loc.branch>=morph_.num_branches()) {
+        throw morphology_error(util::pprintf("Location {} does not exist in morpology", loc));
+    }
+
+    // Test if location is at the start of a branch.
+    if (loc.pos==0.) {
+        auto p = morph_.branch_parent(loc.branch);
+        return p==mnpos? root(): mlocation{p, 1};
+    }
+    return loc;
+}
+
+} // namespace arb
diff --git a/arbor/morph/em_morphology.hpp b/arbor/morph/em_morphology.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5b2f3dc4a5fab0ed403bb998f7d07352372907a7
--- /dev/null
+++ b/arbor/morph/em_morphology.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <vector>
+
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/primitives.hpp>
+
+namespace arb {
+
+class em_morphology {
+    morphology morph_;
+
+    mlocation_list terminals_;
+    mlocation_list forks_;
+    mlocation_list sample_locs_;
+
+    // distance from sample to root
+    std::vector<double> dist2root_;
+    std::vector<double> branch_lengths_;
+
+public:
+    em_morphology(const morphology& m);
+
+    const morphology& morph() const;
+
+    mlocation_list terminals() const;
+    mlocation root() const;
+
+    mlocation sample2loc(msize_t sid) const;
+
+    mlocation canonicalize(mlocation) const;
+
+    // Find all locations on the morphology that share the same canonoical
+    // representation of loc.
+    // If include_loc is false, the input location is excluded from the result.
+    mlocation_list cover(mlocation, bool include_loc=true) const;
+};
+
+} // namespace arb
diff --git a/arbor/morph/label_dict.cpp b/arbor/morph/label_dict.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a6bf10c8a546f83bd0fed9866eb00a0fff0d85e2
--- /dev/null
+++ b/arbor/morph/label_dict.cpp
@@ -0,0 +1,76 @@
+#include <algorithm>
+#include <set>
+#include <fstream>
+#include <unordered_map>
+
+#include <arbor/morph/error.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/label_dict.hpp>
+#include <arbor/morph/region.hpp>
+
+#include "util/filter.hpp"
+#include "util/span.hpp"
+#include "util/strprintf.hpp"
+#include "io/sepval.hpp"
+
+namespace arb {
+
+size_t label_dict::size() const {
+    return locsets_.size() + regions_.size();
+}
+
+void label_dict::set(const std::string& name, arb::locset ls) {
+    if (regions_.count(name)) {
+        throw morphology_error(util::pprintf(
+                "Attempt to add a locset \"{}\" to a label dictionary that already contains a region with the same name.", name));
+    }
+    // First remove an entry with the same name if it exists.
+    // Has to be this way, because insert_or_assign() is C++17, and we
+    // can't use operator[] because locset is not default constructable.
+    auto it = locsets_.find(name);
+    if (it!=locsets_.end()) {
+        it->second = std::move(ls);
+    }
+    else {
+        locsets_.emplace(name, std::move(ls));
+    }
+}
+
+void label_dict::set(const std::string& name, arb::region reg) {
+    if (locsets_.count(name)) {
+        throw morphology_error(util::pprintf(
+                "Attempt to add a region \"{}\" to a label dictionary that already contains a locset with the same name.", name));
+    }
+    // First remove an entry with the same name if it exists.
+    // Has to be this way, because insert_or_assign() is C++17, and we
+    // can't use operator[] because region is not default constructable.
+    auto it = regions_.find(name);
+    if (it!=regions_.end()) {
+        it->second = std::move(reg);
+    }
+    else {
+        regions_.emplace(name, std::move(reg));
+    }
+}
+
+util::optional<const region&> label_dict::region(const std::string& name) const {
+    auto it = regions_.find(name);
+    if (it==regions_.end()) return {};
+    return it->second;
+}
+
+util::optional<const locset&> label_dict::locset(const std::string& name) const {
+    auto it = locsets_.find(name);
+    if (it==locsets_.end()) return {};
+    return it->second;
+}
+
+const std::unordered_map<std::string, locset>& label_dict::locsets() const {
+    return locsets_;
+}
+
+const std::unordered_map<std::string, region>& label_dict::regions() const {
+    return regions_;
+}
+
+} // namespace arb
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..f1215c4eaf2fa301988eff86736e373b7d8cda09
--- /dev/null
+++ b/arbor/morph/locset.cpp
@@ -0,0 +1,247 @@
+#include <algorithm>
+#include <iostream>
+#include <numeric>
+
+#include <arbor/morph/error.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/region.hpp>
+
+#include "util/rangeutil.hpp"
+#include "util/strprintf.hpp"
+
+#include "morph/em_morphology.hpp"
+
+namespace arb {
+namespace ls {
+
+//
+// Functions for taking the sum, union and intersection of location_lists (multisets).
+//
+
+using it_t = mlocation_list::iterator;
+using const_it_t = mlocation_list::const_iterator;
+
+// Advance an iterator to the first value that is not equal to its current
+// value, or end, whichever comes first.
+template <typename T>
+T next_unique(T& it, T end) {
+    const auto& x = *it;
+    ++it;
+    while (it!=end && *it==x) ++it;
+    return it;
+};
+
+// Return the number of times that the value at it is repeated. Advances the
+// iterator to the first value not equal to its current value, or end,
+// whichever comse first.
+template <typename T>
+int multiplicity(T& it, T end) {
+    const auto b = it;
+    return std::distance(b, next_unique(it, end));
+};
+
+mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list v;
+    v.resize(lhs.size() + rhs.size());
+    std::merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), v.begin());
+    return v;
+}
+
+mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list L;
+    L.reserve(lhs.size()+rhs.size());
+
+    auto l    = lhs.begin();
+    auto lend = lhs.end();
+    auto r    = rhs.begin();
+    auto rend = rhs.end();
+
+    auto at_end = [&]() { return l==lend || r==rend; };
+    while (!at_end()) {
+        auto x = (*l<*r) ? *l: *r;
+        auto count = (*l<*r)? multiplicity(l, lend):
+                     (*r<*l)? multiplicity(r, rend):
+                     std::max(multiplicity(l, lend), multiplicity(r, rend));
+        L.insert(L.end(), count, x);
+    }
+    L.insert(L.end(), l, lend);
+    L.insert(L.end(), r, rend);
+
+    return L;
+}
+
+mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs) {
+    mlocation_list L;
+    L.reserve(lhs.size()+rhs.size());
+
+    auto l    = lhs.begin();
+    auto lend = lhs.end();
+    auto r    = rhs.begin();
+    auto rend = rhs.end();
+
+    auto at_end = [&]() { return l==lend || r==rend; };
+    while (!at_end()) {
+        if (*l==*r) {
+            auto x = *l;
+            auto count = std::min(multiplicity(l, lend), multiplicity(r, rend));
+            L.insert(L.end(), count, x);
+        }
+        else if (*l<*r) {
+            next_unique(l, lend);
+        }
+        else {
+            next_unique(r, rend);
+        }
+    }
+
+    return L;
+}
+
+// Null set
+struct nil_ {};
+
+locset nil() {
+    return locset{nil_{}};
+}
+
+mlocation_list thingify_(const nil_& x, const em_morphology& m) {
+    return {};
+}
+
+std::ostream& operator<<(std::ostream& o, const nil_& x) {
+    return o << "nil";
+}
+
+// An explicit location
+struct location_ {
+    mlocation loc;
+};
+
+locset location(mlocation loc) {
+    if (!test_invariants(loc)) {
+        throw morphology_error(util::pprintf("invalid location {}", loc));
+    }
+    return locset{location_{loc}};
+}
+
+mlocation_list thingify_(const location_& x, const em_morphology& m) {
+    // canonicalize will throw if the location is not present.
+    return {m.canonicalize(x.loc)};
+}
+
+std::ostream& operator<<(std::ostream& o, const location_& x) {
+    return o << "(location " << x.loc.branch << " " << x.loc.pos << ")";
+}
+
+
+// Location corresponding to a sample id
+struct sample_ {
+    msize_t index;
+};
+
+locset sample(msize_t index) {
+    return locset{sample_{index}};
+}
+
+mlocation_list thingify_(const sample_& x, const em_morphology& m) {
+    return {m.sample2loc(x.index)};
+}
+
+std::ostream& operator<<(std::ostream& o, const sample_& x) {
+    return o << "(sample " << x.index << ")";
+}
+
+// set of terminal nodes on a morphology
+struct terminal_ {};
+
+locset terminal() {
+    return locset{terminal_{}};
+}
+
+mlocation_list thingify_(const terminal_&, const em_morphology& m) {
+    return m.terminals();
+}
+
+std::ostream& operator<<(std::ostream& o, const terminal_& x) {
+    return o << "terminal";
+}
+
+// the root node of a morphology
+struct root_ {};
+
+locset root() {
+    return locset{root_{}};
+}
+
+mlocation_list thingify_(const root_&, const em_morphology& m) {
+    return {m.root()};
+}
+
+std::ostream& operator<<(std::ostream& o, const root_& x) {
+    return o << "root";
+}
+
+// intersection of two point sets
+struct land {
+    locset lhs;
+    locset rhs;
+    land(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
+};
+
+mlocation_list thingify_(const land& P, const em_morphology& m) {
+    return intersection(thingify(P.lhs, m), thingify(P.rhs, m));
+}
+
+std::ostream& operator<<(std::ostream& o, const land& x) {
+    return o << "(intersect " << x.lhs << " " << x.rhs << ")";
+}
+
+// union of two point sets
+struct lor {
+    locset lhs;
+    locset rhs;
+    lor(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
+};
+
+mlocation_list thingify_(const lor& P, const em_morphology& m) {
+    return join(thingify(P.lhs, m), thingify(P.rhs, m));
+}
+
+std::ostream& operator<<(std::ostream& o, const lor& x) {
+    return o << "(join " << x.lhs << " " << x.rhs << ")";
+}
+
+// sum of two point sets
+struct lsum {
+    locset lhs;
+    locset rhs;
+    lsum(locset lhs, locset rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
+};
+
+mlocation_list thingify_(const lsum& P, const em_morphology& m) {
+    return sum(thingify(P.lhs, m), thingify(P.rhs, m));
+}
+
+std::ostream& operator<<(std::ostream& o, const lsum& x) {
+    return o << "(sum " << x.lhs << " " << x.rhs << ")";
+}
+
+} // namespace ls
+
+// The intersect and join operations in the arb:: namespace with locset so that
+// ADL allows for construction of expressions with locsets without having
+// to namespace qualify the intersect/join.
+
+locset intersect(locset lhs, locset rhs) {
+    return locset(ls::land(std::move(lhs), std::move(rhs)));
+}
+
+locset join(locset lhs, locset rhs) {
+    return locset(ls::lor(std::move(lhs), std::move(rhs)));
+}
+
+locset sum(locset lhs, locset rhs) {
+    return locset(ls::lsum(std::move(lhs), std::move(rhs)));
+}
+
+} // namespace arb
diff --git a/arbor/morph/mbranch.cpp b/arbor/morph/mbranch.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b52b1e1372912300fd3da78e528b1d4548e3dc54
--- /dev/null
+++ b/arbor/morph/mbranch.cpp
@@ -0,0 +1,29 @@
+#include <ostream>
+
+#include <arbor/morph/primitives.hpp>
+
+#include "io/sepval.hpp"
+#include "morph/mbranch.hpp"
+
+namespace arb {
+namespace impl{
+
+//
+//  mbranch implementation
+//
+
+bool operator==(const mbranch& l, const mbranch& r) {
+    return l.parent_id==r.parent_id && l.index==r.index;
+}
+
+std::ostream& operator<<(std::ostream& o, const mbranch& b) {
+    o <<"mbranch([" << io::csv(b.index) << "], ";
+    if (b.parent_id==mnpos) o << "none)";
+    else  o << b.parent_id << ")";
+    return o;
+}
+
+
+} // namespace impl
+} // namespace arb
+
diff --git a/arbor/morph/mbranch.hpp b/arbor/morph/mbranch.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..5d680d1d23af1ac9fd7de79f8ba302c868643042
--- /dev/null
+++ b/arbor/morph/mbranch.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+/*
+ * Definitions and prototypes used in the morphology implementation.
+ * This header is used to simplify unit testing of the morphology implementation.
+ */
+
+#include <cmath>
+#include <vector>
+
+#include <arbor/morph/primitives.hpp>
+
+namespace arb {
+namespace impl{
+
+// An unbranched cable segment that has root, terminal or fork point at each end.
+struct mbranch {
+    std::vector<msize_t> index;  // sample index
+    msize_t parent_id = mnpos;   // branch index
+
+    mbranch() = default;
+    mbranch(std::vector<msize_t> idx, msize_t parent):
+        index(std::move(idx)), parent_id(parent) {}
+
+    bool is_sphere()  const { return size()==1u; }
+    msize_t size()    const { return index.size(); }
+    bool has_parent() const { return parent_id!=mnpos;}
+
+    friend bool operator==(const mbranch& l, const mbranch& r);
+    friend std::ostream& operator<<(std::ostream& o, const mbranch& b);
+};
+
+std::vector<mbranch> branches_from_parent_index(const std::vector<msize_t>& parents,
+                                                const std::vector<point_prop>& props,
+                                                bool spherical_root);
+
+} // namespace impl
+} // namespace arb
diff --git a/arbor/morph/morphology.cpp b/arbor/morph/morphology.cpp
index 23d38c1d2cb7448d863828f0ba83ee6124b42c33..35a73def3deffcf56ec3c0b45146cabde9f6c6d4 100644
--- a/arbor/morph/morphology.cpp
+++ b/arbor/morph/morphology.cpp
@@ -11,6 +11,7 @@
 
 #include "algorithms.hpp"
 #include "io/sepval.hpp"
+#include "morph/mbranch.hpp"
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
 
@@ -93,32 +94,46 @@ bool root_sample_has_same_tag_as_child(const sample_tree& st) {
 
 } // namespace impl
 
-bool operator==(const mbranch& l, const mbranch& r) {
-    return l.parent_id==r.parent_id && l.index==r.index;
-}
+//
+//  morphology_impl definition and implementation
+//
 
-std::ostream& operator<<(std::ostream& o, const mbranch& b) {
-    o <<"mbranch([" << io::csv(b.index) << "], ";
-    if (b.parent_id==mnpos) o << "none)";
-    else  o << b.parent_id << ")";
-    return o;
-}
+struct morphology_impl {
+    // The sample tree of sample points and their parent-child relationships.
+    sample_tree samples_;
 
-morphology::morphology(sample_tree m, bool use_spherical_root):
+    // Indicates whether the soma is a sphere.
+    bool spherical_root_ = false;
+
+    // Branch state.
+    std::vector<impl::mbranch> branches_;
+    std::vector<msize_t> branch_parents_;
+    std::vector<msize_t> root_children_;
+    std::vector<std::vector<msize_t>> branch_children_;
+
+    morphology_impl(sample_tree m, bool use_spherical_root);
+    morphology_impl(sample_tree m);
+
+    void init();
+
+    friend std::ostream& operator<<(std::ostream&, const morphology_impl&);
+};
+
+morphology_impl::morphology_impl(sample_tree m, bool use_spherical_root):
     samples_(std::move(m)),
     spherical_root_(use_spherical_root)
 {
     init();
 }
 
-morphology::morphology(sample_tree m):
+morphology_impl::morphology_impl(sample_tree m):
     samples_(std::move(m)),
     spherical_root_(impl::root_sample_has_same_tag_as_child(samples_))
 {
     init();
 }
 
-void morphology::init() {
+void morphology_impl::init() {
     using util::make_span;
     using util::count_along;
 
@@ -139,50 +154,78 @@ void morphology::init() {
         if (id!=mnpos) {
             branch_children_[id].push_back(i);
         }
+        else {
+            root_children_.push_back(i);
+        }
     }
 }
 
+std::ostream& operator<<(std::ostream& o, const morphology_impl& m) {
+    o << "morphology: "
+      << m.samples_.size() << " samples, "
+      << m.branches_.size() << " branches.";
+    for (auto i: util::make_span(m.branches_.size()))
+        o << "\n  branch " << i << ": " << m.branches_[i];
+
+    return o;
+}
+
+//
+// morphology implementation
+//
+
+morphology::morphology(sample_tree m, bool use_spherical_root):
+    impl_(std::make_shared<const morphology_impl>(std::move(m), use_spherical_root))
+{}
+
+morphology::morphology(sample_tree m):
+    impl_(std::make_shared<const morphology_impl>(std::move(m)))
+{}
+
 // The parent branch of branch b.
 msize_t morphology::branch_parent(msize_t b) const {
-    return branch_parents_[b];
+    return impl_->branch_parents_[b];
 }
 
 // The parent sample of sample i.
 const std::vector<msize_t>& morphology::sample_parents() const {
-    return samples_.parents();
+    return impl_->samples_.parents();
 }
 
 // The child branches of branch b.
 const std::vector<msize_t>& morphology::branch_children(msize_t b) const {
-    return branch_children_[b];
+    return b==mnpos? impl_->root_children_: impl_->branch_children_[b];
 }
 
 // Whether the root of the morphology is spherical.
 bool morphology::spherical_root() const {
-    return spherical_root_;
+    return impl_->spherical_root_;
 }
 
-morphology::index_range morphology::branch_sample_span(msize_t b) const {
-    const auto& idx = branches_[b].index;
+mindex_range morphology::branch_indexes(msize_t b) const {
+    const auto& idx = impl_->branches_[b].index;
     return std::make_pair(idx.data(), idx.data()+idx.size());
 }
 
 const std::vector<msample>& morphology::samples() const {
-    return samples_.samples();
+    return impl_->samples_.samples();
+}
+
+// Point properties of samples in the morphology.
+const std::vector<point_prop>& morphology::sample_props() const {
+    return impl_->samples_.properties();
+}
+
+msize_t morphology::num_samples() const {
+    return impl_->samples_.size();
 }
 
 msize_t morphology::num_branches() const {
-    return branches_.size();
+    return impl_->branches_.size();
 }
 
 std::ostream& operator<<(std::ostream& o, const morphology& m) {
-    o << "morphology: "
-      << m.samples_.size() << " samples, "
-      << m.num_branches() << " branches.";
-    for (auto i: util::make_span(m.num_branches()))
-        o << "\n  branch " << i << ": " << m.branches_[i];
-
-    return o;
+    return o << *m.impl_;
 }
 
 } // namespace arb
diff --git a/arbor/morph/primitives.cpp b/arbor/morph/primitives.cpp
index 6decddbe1c93fa5c2ee35da12d59a9001fff77e8..486959bb60cd2cef279dd54ec5d6806c433a1d51 100644
--- a/arbor/morph/primitives.cpp
+++ b/arbor/morph/primitives.cpp
@@ -28,7 +28,7 @@ double distance(const mpoint& a, const mpoint& b) {
     double dy = a.y - b.y;
     double dz = a.z - b.z;
 
-    return std::sqrt(dx*dx + dy*dy * dz*dz);
+    return std::sqrt(dx*dx + dy*dy + dz*dz);
 }
 
 bool is_collocated(const msample& a, const msample& b) {
@@ -39,12 +39,49 @@ double distance(const msample& a, const msample& b) {
     return distance(a.loc, b.loc);
 }
 
+bool test_invariants(const mlocation& l) {
+    return (0.<=l.pos && l.pos<=1.) && l.branch!=mnpos;
+}
+
+bool test_invariants(const mcable& c) {
+    return (0.<=c.prox_pos && c.prox_pos<=c.dist_pos && c.dist_pos<=1.) && c.branch!=mnpos;
+}
+
+mlocation prox_loc(const mcable& c) {
+    return {c.branch, c.prox_pos};
+}
+
+mlocation dist_loc(const mcable& c) {
+    return {c.branch, c.dist_pos};
+}
+
+bool test_invariants(const mcable_list& l) {
+    return std::is_sorted(l.begin(), l.end())
+        && l.end()==std::find_if(l.begin(), l.end(), [](const mcable& c) {return !test_invariants(c);});
+}
+
 std::ostream& operator<<(std::ostream& o, const mpoint& p) {
-    return o << "mpoint(" << p.x << "," << p.y << "," << p.z << "," << p.radius << ")";
+    return o << "(point " << p.x << " " << p.y << " " << p.z << " " << p.radius << ")";
 }
 
 std::ostream& operator<<(std::ostream& o, const msample& s) {
-    return o << "msample(" << s.loc << ", " << s.tag << ")";
+    return o << "(sample " << s.loc << " " << s.tag << ")";
+}
+
+std::ostream& operator<<(std::ostream& o, const mlocation& l) {
+    return o << "(location " << l.branch << " " << l.pos << ")";
+}
+
+std::ostream& operator<<(std::ostream& o, const mlocation_list& l) {
+    return o << "(list " << io::sepval(l, ' ') << ")";
+}
+
+std::ostream& operator<<(std::ostream& o, const mcable& c) {
+    return o << "(cable " << c.branch << " " << c.prox_pos << " " << c.dist_pos << ")";
+}
+
+std::ostream& operator<<(std::ostream& o, const mcable_list& c) {
+    return o << "(list " << io::sepval(c, ' ') << ")";
 }
 
 } // namespace arb
diff --git a/arbor/morph/region.cpp b/arbor/morph/region.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..90d999b7fe0a5fcbf4a78aff428987e23c187d61
--- /dev/null
+++ b/arbor/morph/region.cpp
@@ -0,0 +1,303 @@
+#include <set>
+#include <string>
+#include <vector>
+
+#include <arbor/morph/error.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/primitives.hpp>
+#include <arbor/morph/region.hpp>
+
+#include "morph/em_morphology.hpp"
+#include "util/span.hpp"
+#include "util/strprintf.hpp"
+#include "util/range.hpp"
+#include "util/rangeutil.hpp"
+
+namespace arb {
+namespace reg {
+
+// Returns true iff cable sections a and b:
+//  1. are on the same branch
+//  2. overlap, i.e. their union is not empty.
+bool is_disjoint(const mcable& a, const mcable& b) {
+    if (a.branch!=b.branch) return true;
+    return a<b? a.dist_pos<b.prox_pos: b.dist_pos<a.prox_pos;
+}
+
+// Take the union of two cable sections that are not disjoint.
+mcable make_union(const mcable& a, const mcable& b) {
+    assert(!is_disjoint(a,b));
+    return mcable{a.branch, std::min(a.prox_pos, b.prox_pos), std::max(a.dist_pos, b.dist_pos)};
+}
+
+// Take the intersection of two cable sections that are not disjoint.
+mcable make_intersection(const mcable& a, const mcable& b) {
+    assert(!is_disjoint(a,b));
+
+    return mcable{a.branch, std::max(a.prox_pos, b.prox_pos), std::min(a.dist_pos, b.dist_pos)};
+}
+
+mcable_list merge(const mcable_list& v) {
+    if (v.size()<2) return v;
+    std::vector<mcable> L;
+    L.reserve(v.size());
+    auto c = v.front();
+    auto it = v.begin()+1;
+    while (it!=v.end()) {
+        if (!is_disjoint(c, *it)) {
+            c = make_union(c, *it);
+        }
+        else {
+            L.push_back(c);
+            c = *it;
+        }
+        ++it;
+    }
+    L.push_back(c);
+    return L;
+}
+
+// Insert a zero-length region at the start of each child branch for every cable
+// that includes the end of a branch.
+// Insert a zero-length region at the end of the parent branch for each cable
+// that includes the start of the branch.
+mcable_list cover(mcable_list cables, const em_morphology& m) {
+    mcable_list L;
+    for (auto& c: cables) {
+        if (c.prox_pos==0) {
+            for (auto& x: m.cover(mlocation{c.branch, 0}, false)) {
+                L.push_back({x.branch, x.pos, x.pos});
+            }
+        }
+        if (c.dist_pos==1) {
+            for (auto& x: m.cover(mlocation{c.branch, 1}, false)) {
+                L.push_back({x.branch, x.pos, x.pos});
+            }
+        }
+    }
+    L.insert(L.end(), cables.begin(), cables.end());
+    util::sort(L);
+
+    return L;
+}
+
+mcable_list remove_cover(mcable_list cables, const em_morphology& m) {
+    // Find all zero-length cables at the end of cables, and convert to
+    // their canonical representation.
+    for (auto& c: cables) {
+        if (c.dist_pos==0 || c.prox_pos==1) {
+            auto cloc = m.canonicalize({c.branch, c.prox_pos});
+            c = {cloc.branch, cloc.pos, cloc.pos};
+        }
+    }
+    // Some new zero-length cables may be out of order, so sort
+    // the cables.
+    util::sort(cables);
+
+    // Remove multiple copies of zero-length cables if present.
+    return merge(cables);
+}
+
+//
+// Explicit cable section
+//
+
+struct cable_ {
+    mcable cable;
+};
+
+region cable(mcable c) {
+    if (!test_invariants(c)) {
+        throw morphology_error(util::pprintf("Invalid cable section {}", c));
+    }
+    return region(cable_{c});
+}
+
+region branch(msize_t bid) {
+    return cable({bid, 0, 1});
+}
+
+mcable_list thingify_(const cable_& reg, const em_morphology& em) {
+    auto& m = em.morph();
+
+    if (reg.cable.branch>=m.num_branches()) {
+        throw morphology_error(util::pprintf("Branch {} does not exist in morpology", reg.cable.branch));
+    }
+
+    return {reg.cable};
+}
+
+std::ostream& operator<<(std::ostream& o, const cable_& c) {
+    return o << c.cable;
+}
+
+//
+// region with all segments with the same numeric tag
+//
+struct tagged_ {
+    int tag;
+};
+
+region tagged(int id) {
+    return region(tagged_{id});
+}
+
+mcable_list thingify_(const tagged_& reg, const em_morphology& em) {
+    auto& m = em.morph();
+    size_t nb = m.num_branches();
+
+    std::vector<mcable> L;
+    L.reserve(nb);
+    auto& samples = m.samples();
+    auto matches     = [reg, &samples](msize_t i) {return samples[i].tag==reg.tag;};
+    auto not_matches = [&matches](msize_t i) {return !matches(i);};
+
+    for (msize_t i: util::make_span(nb)) {
+        auto ids = util::make_range(m.branch_indexes(i)); // range of sample ids in branch.
+        size_t ns = util::size(ids);        // number of samples in branch.
+
+        if (ns==1) {
+            // The branch is a spherical soma
+            if (samples[0].tag==reg.tag) {
+                L.push_back({0,0,1});
+            }
+            continue;
+        }
+
+        // The branch has at least 2 samples.
+        // Start at begin+1 because a segment gets its tag from its distal sample.
+        auto beg = std::cbegin(ids);
+        auto end = std::cend(ids);
+
+        // Find the next sample that matches reg.tag.
+        auto start = std::find_if(beg+1, end, matches);
+        while (start!=end) {
+            // find the next sample that does not match reg.tag
+            auto first = start-1;
+            auto last = std::find_if(start, end, not_matches);
+
+            auto l = first==beg? 0.: em.sample2loc(*first).pos;
+            auto r = last==end?  1.: em.sample2loc(*(last-1)).pos;
+            L.push_back({i, l, r});
+
+            // Find the next sample in the branch that matches reg.tag.
+            start = std::find_if(last, end, matches);
+        }
+    }
+    if (L.size()<L.capacity()/4) {
+        L.shrink_to_fit();
+    }
+    return L;
+}
+
+std::ostream& operator<<(std::ostream& o, const tagged_& t) {
+    return o << "(tag " << t.tag << ")";
+}
+
+//
+// region with all segments in a cell
+//
+struct all_ {};
+
+region all() {
+    return region(all_{});
+}
+
+mcable_list thingify_(const all_&, const em_morphology& m) {
+    auto nb = m.morph().num_branches();
+    mcable_list branches;
+    branches.reserve(nb);
+    for (auto i: util::make_span(nb)) {
+        branches.push_back({i,0,1});
+    }
+    return branches;
+}
+
+std::ostream& operator<<(std::ostream& o, const all_& t) {
+    return o << "all";
+}
+
+//
+// intersection of two regions.
+//
+struct reg_and {
+    region lhs;
+    region rhs;
+    reg_and(region lhs, region rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
+};
+
+mcable_list thingify_(const reg_and& P, const em_morphology& m) {
+    using cable_it = std::vector<mcable>::const_iterator;
+    using cable_it_pair = std::pair<cable_it, cable_it>;
+
+    auto lhs = cover(thingify(P.lhs, m), m);
+    auto rhs = cover(thingify(P.rhs, m), m);
+
+    // Perform intersection
+    cable_it_pair it{lhs.begin(), rhs.begin()};
+    cable_it_pair end{lhs.end(), rhs.end()};
+    std::vector<mcable> L;
+
+    bool at_end = it.first==end.first || it.second==end.second;
+    while (!at_end) {
+        bool first_less = *(it.first) < *(it.second);
+        auto& lhs = first_less? it.first: it.second;
+        auto& rhs = first_less? it.second: it.first;
+        if (!is_disjoint(*lhs, *rhs)) {
+            L.push_back(make_intersection(*lhs, *rhs));
+        }
+        if (dist_loc(*lhs) < dist_loc(*rhs)) {
+            ++lhs;
+        }
+        else {
+            ++rhs;
+        }
+        at_end = it.first==end.first || it.second==end.second;
+    }
+
+    return remove_cover(L, m);
+}
+
+std::ostream& operator<<(std::ostream& o, const reg_and& x) {
+    return o << "(intersect " << x.lhs << " " << x.rhs << ")";
+}
+
+//
+// union of two point sets
+//
+struct reg_or {
+    region lhs;
+    region rhs;
+    reg_or(region lhs, region rhs): lhs(std::move(lhs)), rhs(std::move(rhs)) {}
+};
+
+mcable_list thingify_(const reg_or& P, const em_morphology& m) {
+    auto lhs = thingify(P.lhs, m);
+    auto rhs = thingify(P.rhs, m);
+    mcable_list L;
+    L.resize(lhs.size() + rhs.size());
+
+    std::merge(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), L.begin());
+
+    return merge(L);
+}
+
+std::ostream& operator<<(std::ostream& o, const reg_or& x) {
+    return o << "(join " << x.lhs << " " << x.rhs << ")";
+}
+
+} // namespace reg
+
+// The intersect and join operations in the arb:: namespace with region so that
+// ADL allows for construction of expressions with regions without having
+// to namespace qualify the intersect/join.
+
+region intersect(region l, region r) {
+    return region{reg::reg_and(std::move(l), std::move(r))};
+}
+
+region join(region l, region r) {
+    return region{reg::reg_or(std::move(l), std::move(r))};
+}
+
+} // namespace arb
diff --git a/arbor/morph/sample_tree.cpp b/arbor/morph/sample_tree.cpp
index 7a687e52ca9eb966470fbdd23f2c9d14ff010c93..d8f82c10b33652407a32f7cde86c6331653d2417 100644
--- a/arbor/morph/sample_tree.cpp
+++ b/arbor/morph/sample_tree.cpp
@@ -70,7 +70,6 @@ msize_t sample_tree::append(msize_t p, const msample& s) {
 }
 
 msize_t sample_tree::append(const msample& s) {
-    std::cout << "decision : " << empty() << " " << (empty()? mnpos: size()-1) << "\n";
     return append(empty()? mnpos: size()-1, s);
 }
 
diff --git a/doc/cpp_cable_cell.rst b/doc/cpp_cable_cell.rst
index 4cf1dce867ae1eeae7a954ce6afde27f96db56a6..56c42a4223ba3d40295a11c200930170260c0553 100644
--- a/doc/cpp_cable_cell.rst
+++ b/doc/cpp_cable_cell.rst
@@ -97,13 +97,11 @@ Density mechanisms are associated with a cable cell object with:
 Point mechanisms, which are associated with connection end points on a
 cable cell, are attached to a cell with:
 
-.. cpp:function:: void cable_cell::add_synapse(segment_location loc, mechanism_desc mech)
+.. cpp:function:: void cable_cell::add_synapse(mlocation loc, mechanism_desc mech)
 
-where :cpp:type:`segment_location` is a simple struct holding a segment index
+where :cpp:type:`mlocation` is a simple struct holding a segment index
 and a relative position (from 0, proximal, to 1, distal) along that segment:
 
-.. cpp:function:: segment_location::segment_location(cell_lid_type index, double position)
-
 
 Electrical properities and ion values
 -------------------------------------
diff --git a/example/dryrun/dryrun.cpp b/example/dryrun/dryrun.cpp
index 42b4ca7e4ef77673aaa57105fea3d5848dfc715f..e54bc1a70faeb29bc30a3e001e2dbeb1f18678d3 100644
--- a/example/dryrun/dryrun.cpp
+++ b/example/dryrun/dryrun.cpp
@@ -12,8 +12,9 @@
 #include <arbor/assert_macro.hpp>
 #include <arbor/common_types.hpp>
 #include <arbor/context.hpp>
-#include <arbor/load_balance.hpp>
 #include <arbor/cable_cell.hpp>
+#include <arbor/load_balance.hpp>
+#include <arbor/morph/primitives.hpp>
 #include <arbor/profile/meter_manager.hpp>
 #include <arbor/profile/profiler.hpp>
 #include <arbor/simple_sampler.hpp>
@@ -140,7 +141,7 @@ public:
         // Get the appropriate kind for measuring voltage.
         cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
         // Measure at the soma.
-        arb::segment_location loc(0, 0.0);
+        arb::mlocation loc{0, 0.0};
 
         return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
     }
diff --git a/example/gap_junctions/gap_junctions.cpp b/example/gap_junctions/gap_junctions.cpp
index f3e1e26ee9c867189abdd823747ddf41d287d33e..8212beec6b8d181de0cf96d089b6dd59c96d007a 100644
--- a/example/gap_junctions/gap_junctions.cpp
+++ b/example/gap_junctions/gap_junctions.cpp
@@ -103,7 +103,7 @@ public:
         // Get the appropriate kind for measuring voltage.
         cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
         // Measure at the soma.
-        arb::segment_location loc(0, 1);
+        arb::mlocation loc{0, 1.};
 
         return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
     }
diff --git a/example/generators/generators.cpp b/example/generators/generators.cpp
index b7acd24a06cea07a0bdd2a8e418c2f03f8217b00..9c18c5deba2430698db2ee81c934ce1de2335b62 100644
--- a/example/generators/generators.cpp
+++ b/example/generators/generators.cpp
@@ -123,7 +123,7 @@ public:
         // Get the appropriate kind for measuring voltage
         cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
         // Measure at the soma
-        arb::segment_location loc(0, 0.0);
+        arb::mlocation loc{0, 0.0};
 
         return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
     }
diff --git a/example/generators/readme.md b/example/generators/readme.md
index 0f2e71f220d83f1d4ffbfbe6996b230b794d9b6d..d8858740da6fa318d8174c899a39f1fad4d84191 100644
--- a/example/generators/readme.md
+++ b/example/generators/readme.md
@@ -156,9 +156,9 @@ In our case, the cell has one probe, which refers to the voltage at the soma.
         // Get the appropriate kind for measuring voltage
         cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
 
-        // The location at which we measure: position 0 on segment 0.
-        // The cell has only one segment, segment 0, which is the soma.
-        arb::segment_location loc(0, 0.0);
+        // The location at which we measure: position 0 on branch 0.
+        // The cell has only one branch, branch 0, which is the soma.
+        arb::mlocation loc(0, 0.0);
 
         // Put this together into a `probe_info`
         return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
diff --git a/example/ring/ring.cpp b/example/ring/ring.cpp
index 6f4260320fc31fa9ed385c0b9fc436e39a77f28d..4cc95891bbe7a41e0c24933a031c1368b7173376 100644
--- a/example/ring/ring.cpp
+++ b/example/ring/ring.cpp
@@ -134,7 +134,7 @@ public:
         // Get the appropriate kind for measuring voltage.
         cell_probe_address::probe_kind kind = cell_probe_address::membrane_voltage;
         // Measure at the soma.
-        arb::segment_location loc(0, 0.0);
+        arb::mlocation loc{0, 0.0};
 
         return arb::probe_info{id, kind, cell_probe_address{loc, kind}};
     }
diff --git a/example/single/single.cpp b/example/single/single.cpp
index df614950c3cf6b2060e48593dc71c92a283c8aca..6673ef09460ca8352d774391c5b0d42b0531e58c 100644
--- a/example/single/single.cpp
+++ b/example/single/single.cpp
@@ -36,7 +36,7 @@ struct single_recipe: public arb::recipe {
     arb::cell_size_type num_targets(arb::cell_gid_type) const override { return 1; }
 
     arb::probe_info get_probe(arb::cell_member_type probe_id) const override {
-        arb::segment_location mid_soma = {0, 0.5};
+        arb::mlocation mid_soma = {0, 0.5};
         arb::cell_probe_address probe = {mid_soma, arb::cell_probe_address::membrane_voltage};
 
         // Probe info consists of: the probe id, a tag value to distinguish this probe
@@ -54,27 +54,29 @@ struct single_recipe: public arb::recipe {
     }
 
     arb::util::unique_any get_cell_description(arb::cell_gid_type) const override {
-        arb::cable_cell c = make_cable_cell(morpho, false);
+        arb::label_dict dict;
+        using arb::reg::tagged;
+        dict.set("soma", tagged(1));
+        dict.set("dend", join(tagged(3), tagged(4), tagged(42)));
+        arb::cable_cell c = make_cable_cell(morpho, dict, false);
 
         // Add HH mechanism to soma, passive channels to dendrites.
+        c.paint("soma", "hh");
+        c.paint("dend", "pas");
+
         // Discretize dendrites according to the NEURON d-lambda rule.
+        for (std::size_t i=1; i<c.num_segments(); ++i) {
+            arb::cable_segment* branch = c.cable(i);
 
-        c.soma()->add_mechanism("hh");
-        auto& segments = c.segments();
-        for (std::size_t i = 1; i<segments.size(); ++i) {
             double dx = c.segment_length_constant(100., i, gprop.default_parameters)*0.3;
-
-            arb::cable_segment* seg = c.cable(i);
-            seg->add_mechanism("pas");
-
-            unsigned n = std::ceil(seg->length()/dx);
-            seg->set_compartments(n);
+            unsigned n = std::ceil(branch->length()/dx);
+            branch->set_compartments(n);
         }
 
         // Add synapse to last branch.
 
-        arb::cell_lid_type last_segment = morpho.num_branches()-1;
-        arb::segment_location end_last_segment = { last_segment, 1. };
+        arb::cell_lid_type last_segment = c.num_segments()-1;
+        arb::mlocation end_last_segment = { last_segment, 1. };
         c.add_synapse(end_last_segment, "exp2syn");
 
         return c;
@@ -151,7 +153,6 @@ arb::morphology default_morphology() {
     arb::sample_tree samples;
 
     auto p = samples.append(arb::msample{{  0.0, 0.0, 0.0, 6.3}, 1});
-    std::cout << "p is " << p << "\n";
     p = samples.append(p, arb::msample{{  6.3, 0.0, 0.0, 0.5}, 3});
     p = samples.append(p, arb::msample{{206.3, 0.0, 0.0, 0.2}, 3});
 
diff --git a/test/unit/CMakeLists.txt b/test/unit/CMakeLists.txt
index 4bebb1919fb99f20bbb3944a5765071d66ba458a..468264f5c12f9c73b7743bb700d2381859dddc66 100644
--- a/test/unit/CMakeLists.txt
+++ b/test/unit/CMakeLists.txt
@@ -72,13 +72,15 @@ set(unit_sources
     test_algorithms.cpp
     test_any.cpp
     test_backend.cpp
-    test_double_buffer.cpp
-    test_dry_run_context.cpp
+    test_cable_cell.cpp
     test_compartments.cpp
     test_counter.cpp
     test_cycle.cpp
     test_domain_decomposition.cpp
+    test_dry_run_context.cpp
+    test_double_buffer.cpp
     test_either.cpp
+    test_em_morphology.cpp
     test_event_binner.cpp
     test_event_delivery.cpp
     test_event_generators.cpp
@@ -87,23 +89,23 @@ set(unit_sources
     test_fvm_layout.cpp
     test_fvm_lowered.cpp
     test_glob_basic.cpp
-    test_mc_cell_group.cpp
     test_kinetic_linear.cpp
     test_lexcmp.cpp
     test_lif_cell_group.cpp
+    test_local_context.cpp
     test_maputil.cpp
     test_mask_stream.cpp
     test_math.cpp
     test_matrix.cpp
-    test_cable_cell.cpp
+    test_mc_cell_group.cpp
     test_mechanisms.cpp
-    test_mech_temperature.cpp
     test_mechcat.cpp
+    test_mechinfo.cpp
+    test_mech_temperature.cpp
     test_merge_events.cpp
+    test_morphology.cpp
     test_multi_event_stream.cpp
     test_optional.cpp
-    test_mechinfo.cpp
-    test_morphology.cpp
     test_padded.cpp
     test_partition.cpp
     test_partition_by_constraint.cpp
@@ -114,7 +116,6 @@ set(unit_sources
     test_segment.cpp
     test_schedule.cpp
     test_spike_source.cpp
-    test_local_context.cpp
     test_scope_exit.cpp
     test_simd.cpp
     test_span.cpp
diff --git a/test/unit/test_em_morphology.cpp b/test/unit/test_em_morphology.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c724f5d1f9b772df2893d1c1a7e71a13ceccf094
--- /dev/null
+++ b/test/unit/test_em_morphology.cpp
@@ -0,0 +1,627 @@
+/*
+ * Unit tests for em_morphology, region, locset, label_dict.
+ */
+
+#include <cmath>
+#include <string>
+#include <vector>
+
+#include "../test/gtest.h"
+
+#include <arbor/math.hpp>
+#include <arbor/morph/error.hpp>
+#include <arbor/morph/morphology.hpp>
+#include <arbor/morph/sample_tree.hpp>
+#include <arbor/morph/locset.hpp>
+#include <arbor/morph/region.hpp>
+
+#include "io/sepval.hpp"
+#include "morph/em_morphology.hpp"
+#include "util/span.hpp"
+
+template <typename T>
+std::ostream& operator<<(std::ostream& o, const std::vector<T>& v) {
+    return o << "[" << arb::io::csv(v) << "]";
+}
+
+// Test the morphology meta-data that is cached on construction of
+// em_morpholgy, e.g. interpolation information and terminal nodes.
+TEST(em_morphology, cache) {
+    using pvec = std::vector<arb::msize_t>;
+    using svec = std::vector<arb::msample>;
+    using loc = arb::mlocation;
+    constexpr auto npos = arb::mnpos;
+
+    // A single unbranched cable with 5 sample points.
+    // The cable has length 10 μm, with samples located at
+    // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
+    {
+        pvec parents = {npos, 0, 1, 2, 3};
+        svec samples = {
+            {{  0,  0,  0,  2}, 1},
+            {{  1,  0,  0,  2}, 1},
+            {{  3,  0,  0,  2}, 1},
+            {{  7,  0,  0,  2}, 1},
+            {{ 10,  0,  0,  2}, 1},
+        };
+        arb::sample_tree sm(samples, parents);
+        arb::em_morphology em(arb::morphology(sm, false));
+        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
+        EXPECT_EQ(em.sample2loc(1), (loc{0,0.1}));
+        EXPECT_EQ(em.sample2loc(2), (loc{0,0.3}));
+        EXPECT_EQ(em.sample2loc(3), (loc{0,0.7}));
+        EXPECT_EQ(em.sample2loc(4), (loc{0,1}));
+
+        EXPECT_EQ(em.root(),      (loc{0,0}));
+        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}}));
+    }
+
+    // Eight samples
+    //
+    //  sample ids:
+    //            0
+    //           1 3
+    //          2   4
+    //             5 6
+    //                7
+    {   // Spherical root.
+        pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
+
+        svec samples = {
+            {{  0,  0,  0, 10}, 1},
+            {{ 10,  0,  0,  2}, 3},
+            {{100,  0,  0,  2}, 3},
+            {{  0, 10,  0,  2}, 3},
+            {{  0,100,  0,  2}, 3},
+            {{100,100,  0,  2}, 3},
+            {{  0,200,  0,  2}, 3},
+            {{  0,300,  0,  2}, 3},
+        };
+        arb::sample_tree sm(samples, parents);
+        arb::em_morphology em(arb::morphology(sm, true));
+
+        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
+        EXPECT_EQ(em.sample2loc(1), (loc{1,0}));
+        EXPECT_EQ(em.sample2loc(2), (loc{1,1}));
+        EXPECT_EQ(em.sample2loc(3), (loc{2,0}));
+        EXPECT_EQ(em.sample2loc(4), (loc{2,1}));
+        EXPECT_EQ(em.sample2loc(5), (loc{3,1}));
+        EXPECT_EQ(em.sample2loc(6), (loc{4,0.5}));
+        EXPECT_EQ(em.sample2loc(7), (loc{4,1}));
+
+        EXPECT_EQ(em.root(),      (loc{0,0}));
+        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{1,1}, {3,1}, {4,1}}));
+    }
+    {   // No Spherical root
+        pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
+
+        svec samples = {
+            {{  0,  0,  0,  2}, 1},
+            {{ 10,  0,  0,  2}, 3},
+            {{100,  0,  0,  2}, 3},
+            {{  0, 10,  0,  2}, 3},
+            {{  0,100,  0,  2}, 3},
+            {{100,100,  0,  2}, 3},
+            {{  0,130,  0,  2}, 3},
+            {{  0,300,  0,  2}, 3},
+        };
+        arb::sample_tree sm(samples, parents);
+        arb::em_morphology em(arb::morphology(sm, false));
+
+        EXPECT_EQ(em.sample2loc(0), (loc{0,0}));
+        EXPECT_EQ(em.sample2loc(1), (loc{0,0.1}));
+        EXPECT_EQ(em.sample2loc(2), (loc{0,1}));
+        EXPECT_EQ(em.sample2loc(3), (loc{1,0.1}));
+        EXPECT_EQ(em.sample2loc(4), (loc{1,1}));
+        EXPECT_EQ(em.sample2loc(5), (loc{2,1}));
+        EXPECT_EQ(em.sample2loc(6), (loc{3,0.15}));
+        EXPECT_EQ(em.sample2loc(7), (loc{3,1}));
+
+        EXPECT_EQ(em.root(),      (loc{0,0}));
+        EXPECT_EQ(em.terminals(), (arb::mlocation_list{{0,1}, {2,1}, {3,1}}));
+    }
+}
+
+TEST(em_morphology, cover) {
+    using pvec = std::vector<arb::msize_t>;
+    using svec = std::vector<arb::msample>;
+    using ll = arb::mlocation_list;
+    constexpr auto npos = arb::mnpos;
+    using arb::util::make_span;
+
+    // Eight samples
+    //            0
+    //           1 3
+    //          2   4
+    //             5 6
+    //                7
+    pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
+
+    svec samples = {
+        {{  0,  0,  0, 10}, 1},
+        {{ 10,  0,  0,  2}, 3},
+        {{100,  0,  0,  2}, 3},
+        {{  0, 10,  0,  2}, 3},
+        {{  0,100,  0,  2}, 3},
+        {{100,100,  0,  2}, 3},
+        {{  0,200,  0,  2}, 3},
+        {{  0,300,  0,  2}, 3},
+    };
+    arb::sample_tree sm(samples, parents);
+
+    // non-spherical root.
+    {
+        arb::em_morphology em(arb::morphology(sm, false));
+
+        for (arb::msize_t i: make_span(em.morph().num_branches())) {
+            for (auto j: make_span(1,9)) {
+                arb::mlocation l{i, j/10.};
+                EXPECT_EQ(em.cover(l), (ll{l}));
+            }
+        }
+
+        EXPECT_EQ(em.cover({0,0}), (ll{{0,0}, {1,0}}));
+        EXPECT_EQ(em.cover({0,1}), (ll{{0,1}}));
+        EXPECT_EQ(em.cover({1,1}), (ll{{1,1}, {2,0}, {3,0}}));
+        EXPECT_EQ(em.cover({2,1}), (ll{{2,1}}));
+        EXPECT_EQ(em.cover({3,1}), (ll{{3,1}}));
+
+        EXPECT_EQ(em.cover({1,0}), (ll{{0,0}, {1,0}}));
+        EXPECT_EQ(em.cover({2,0}), (ll{{1,1}, {2,0}, {3,0}}));
+        EXPECT_EQ(em.cover({3,0}), (ll{{1,1}, {2,0}, {3,0}}));
+    }
+    // spherical root.
+    {
+        arb::em_morphology em(arb::morphology(sm, true));
+
+        EXPECT_EQ(em.cover({0,0}), (ll{{0,0}}));
+        EXPECT_EQ(em.cover({0,1}), (ll{{0,1}, {1,0}, {2,0}}));
+        EXPECT_EQ(em.cover({1,1}), (ll{{1,1}}));
+        EXPECT_EQ(em.cover({2,1}), (ll{{2,1}, {3,0}, {4,0}}));
+        EXPECT_EQ(em.cover({3,1}), (ll{{3,1}}));
+        EXPECT_EQ(em.cover({4,1}), (ll{{4,1}}));
+        EXPECT_EQ(em.cover({1,0}), (ll{{0,1}, {1,0}, {2,0}}));
+        EXPECT_EQ(em.cover({2,0}), (ll{{0,1}, {1,0}, {2,0}}));
+        EXPECT_EQ(em.cover({3,0}), (ll{{2,1}, {3,0}, {4,0}}));
+        EXPECT_EQ(em.cover({4,0}), (ll{{2,1}, {3,0}, {4,0}}));
+    }
+}
+
+// Test expressions that describe location sets (locset).
+TEST(locset, expressions) {
+    auto root = arb::ls::root();
+    auto term = arb::ls::terminal();
+    auto samp = arb::ls::sample(42);
+    auto loc = arb::ls::location({2, 0.5});
+
+    auto to_string = [](auto&& x) {
+        std::stringstream s;
+        s << x;
+        return s.str();
+    };
+
+    EXPECT_EQ(to_string(root), "root");
+    EXPECT_EQ(to_string(term), "terminal");
+    EXPECT_EQ(to_string(sum(root, term)), "(sum root terminal)");
+    EXPECT_EQ(to_string(sum(root, term, samp)),
+            "(sum (sum root terminal) (sample 42))");
+    EXPECT_EQ(to_string(sum(root, term, samp, loc)),
+            "(sum (sum (sum root terminal) (sample 42)) (location 2 0.5))");
+    EXPECT_EQ(to_string(samp), "(sample 42)");
+    EXPECT_EQ(to_string(loc), "(location 2 0.5)");
+
+    // Location positions have to be in the range [0,1].
+    // Assert that an exception is thrown if and invalide location is requested.
+    EXPECT_THROW(arb::ls::location({2, 1.5}), arb::morphology_error);
+    EXPECT_THROW(arb::ls::location({arb::mnpos, 0.5}), arb::morphology_error);
+}
+
+// Test expressions that describe regions.
+TEST(region, expressions) {
+    using arb::reg::cable;
+    auto to_string = [](auto&& x) {
+        std::stringstream s;
+        s << x;
+        return s.str();
+    };
+
+    auto c1 = arb::reg::cable({1,0,1});
+    auto c2 = arb::reg::cable({4,0.1,0.5});
+    auto c3 = join(cable({4,0.1,0.5}), cable({3,0,1}));
+    auto b1 = arb::reg::branch(1);
+    auto t1 = arb::reg::tagged(1);
+    auto t2 = arb::reg::tagged(2);
+    auto t3 = arb::reg::tagged(3);
+    auto all = arb::reg::all();
+
+    EXPECT_EQ(to_string(c1), "(cable 1 0 1)");
+    EXPECT_EQ(to_string(c2), "(cable 4 0.1 0.5)");
+    EXPECT_EQ(to_string(c3), "(join (cable 4 0.1 0.5) (cable 3 0 1))");
+    EXPECT_EQ(to_string(b1), "(cable 1 0 1)");
+    EXPECT_EQ(to_string(t1), "(tag 1)");
+    EXPECT_EQ(to_string(t2), "(tag 2)");
+    EXPECT_EQ(to_string(intersect(c1, t2)), "(intersect (cable 1 0 1) (tag 2))");
+    EXPECT_EQ(to_string(join(c1, t2)),  "(join (cable 1 0 1) (tag 2))");
+    EXPECT_EQ(to_string(join(t1, t2, t3)), "(join (join (tag 1) (tag 2)) (tag 3))");
+    EXPECT_EQ(to_string(intersect(t1, t2, t3)), "(intersect (intersect (tag 1) (tag 2)) (tag 3))");
+    EXPECT_EQ(to_string(intersect(join(c1, t2), c2)),  "(intersect (join (cable 1 0 1) (tag 2)) (cable 4 0.1 0.5))");
+    EXPECT_EQ(to_string(all), "all");
+
+    EXPECT_THROW(arb::reg::cable({1, 0, 1.1}), arb::morphology_error);
+    EXPECT_THROW(arb::reg::branch(-1), arb::morphology_error);
+}
+
+TEST(locset, thingify) {
+    using pvec = std::vector<arb::msize_t>;
+    using svec = std::vector<arb::msample>;
+    using ll = arb::mlocation_list;
+    const auto npos = arb::mnpos;
+
+    auto root = arb::ls::root();
+    auto term = arb::ls::terminal();
+    auto samp = arb::ls::sample(4);
+    auto midb2 = arb::ls::location({2, 0.5});
+    auto midb1 = arb::ls::location({1, 0.5});
+    auto begb0 = arb::ls::location({0, 0});
+    auto begb1 = arb::ls::location({1, 0});
+    auto begb2 = arb::ls::location({2, 0});
+    auto begb3 = arb::ls::location({3, 0});
+    auto begb4 = arb::ls::location({4, 0});
+
+    // Eight samples
+    //
+    //            0
+    //           1 3
+    //          2   4
+    //             5 6
+    //                7
+    pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
+    svec samples = {
+        {{  0,  0,  0,  2}, 3},
+        {{ 10,  0,  0,  2}, 3},
+        {{100,  0,  0,  2}, 3},
+        {{  0, 10,  0,  2}, 3},
+        {{  0,100,  0,  2}, 3},
+        {{100,100,  0,  2}, 3},
+        {{  0,200,  0,  2}, 3},
+        {{  0,300,  0,  2}, 3},
+    };
+    arb::sample_tree sm(samples, parents);
+
+    {
+        arb::em_morphology em(arb::morphology(sm, true));
+
+        EXPECT_EQ(thingify(root, em),  (ll{{0,0}}));
+        EXPECT_EQ(thingify(term, em),  (ll{{1,1},{3,1},{4,1}}));
+        EXPECT_EQ(thingify(samp, em),  (ll{{2,1}}));
+        EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}}));
+        EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}}));
+        EXPECT_EQ(thingify(begb0, em), (ll{{0,0}}));
+        EXPECT_EQ(thingify(begb1, em), (ll{{0,1}}));
+        EXPECT_EQ(thingify(begb2, em), (ll{{0,1}}));
+        EXPECT_EQ(thingify(begb3, em), (ll{{2,1}}));
+        EXPECT_EQ(thingify(begb4, em), (ll{{2,1}}));
+    }
+    {
+        arb::em_morphology em(arb::morphology(sm, false));
+
+        EXPECT_EQ(thingify(root, em),  (ll{{0,0}}));
+        EXPECT_EQ(thingify(term, em),  (ll{{0,1},{2,1},{3,1}}));
+        EXPECT_EQ(thingify(samp, em),  (ll{{1,1}}));
+        EXPECT_EQ(thingify(midb2, em), (ll{{2,0.5}}));
+        EXPECT_EQ(thingify(midb1, em), (ll{{1,0.5}}));
+        EXPECT_EQ(thingify(begb0, em), (ll{{0,0}}));
+        EXPECT_EQ(thingify(begb1, em), (ll{{0,0}}));
+        EXPECT_EQ(thingify(begb2, em), (ll{{1,1}}));
+        EXPECT_EQ(thingify(begb3, em), (ll{{1,1}}));
+        // In the absence of a spherical root, there is no branch 4.
+        EXPECT_THROW(thingify(begb4, em), arb::morphology_error);
+    }
+}
+
+// Forward declare implementation of join, union & intersect on location lists
+// for testing.
+namespace arb { namespace ls {
+    mlocation_list intersection(const mlocation_list& lhs, const mlocation_list& rhs);
+    mlocation_list join(const mlocation_list& lhs, const mlocation_list& rhs);
+    mlocation_list sum(const mlocation_list& lhs, const mlocation_list& rhs);
+}}
+
+arb::mlocation_list ml(std::vector<int> bids) {
+    arb::mlocation_list L;
+    for (arb::msize_t b: bids) L.push_back({b, 0});
+    return L;
+}
+
+
+// Test out multiset joins, intersections and sums
+TEST(locset, join_intersect_sum) {
+    using ll = arb::mlocation_list;
+    using arb::ls::intersection;
+    using arb::ls::sum;
+    using arb::ls::join;
+
+    {
+        ll lhs{};
+        ll rhs{};
+        EXPECT_EQ(sum(lhs, rhs), ll{});
+        EXPECT_EQ(join(lhs, rhs), ll{});
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs{};
+        ll rhs = ml({0,1});
+        EXPECT_EQ(sum(lhs, rhs), rhs);
+        EXPECT_EQ(join(lhs, rhs), rhs);
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs = ml({1});
+        ll rhs = ml({1});
+        EXPECT_EQ(sum(lhs,  rhs), ml({1,1}));
+        EXPECT_EQ(join(lhs, rhs), ml({1}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({1,1});
+        ll rhs = ml({1});
+        EXPECT_EQ(sum(lhs,  rhs), ml({1,1,1}));
+        EXPECT_EQ(join(lhs, rhs), ml({1,1}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs,  rhs), ml({0,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ll{});
+    }
+    {
+        ll lhs = ml({0,1,3});
+        ll rhs = ml({0,1,3});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,0,1,1,3,3}));
+        EXPECT_EQ(join(lhs, rhs), lhs);
+        EXPECT_EQ(intersection(lhs, rhs), lhs);
+    }
+    {
+        ll lhs = ml({0,1,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,1,1,3});
+        ll rhs = ml({1,2});
+        EXPECT_EQ(sum(lhs, rhs), ml({0,1,1,1,2,3}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,3}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1}));
+    }
+    {
+        ll lhs = ml({0,1,1,3,5,5,12});
+        ll rhs = ml({1,2,2,5,5,5});
+        EXPECT_EQ(sum(lhs, rhs),  ml({0,1,1,1,2,2,3,5,5,5,5,5,12}));
+        EXPECT_EQ(join(lhs, rhs), ml({0,1,1,2,2,3,5,5,5,12}));
+        EXPECT_EQ(intersection(lhs, rhs), ml({1,5,5}));
+    }
+}
+
+TEST(region, thingify) {
+    using pvec = std::vector<arb::msize_t>;
+    using svec = std::vector<arb::msample>;
+    //using cab = arb::mcable;
+    using cl = arb::mcable_list;
+    constexpr auto npos = arb::mnpos;
+
+    // A single unbranched cable with 5 sample points.
+    // The cable has length 10 μm, with samples located at
+    // 0 μm, 1 μm, 3 μm, 7 μm and 10 μm.
+    {
+        pvec parents = {npos, 0, 1, 2, 3};
+        svec samples = {
+            {{  0,  0,  0,  2}, 1},
+            {{  1,  0,  0,  2}, 1},
+            {{  3,  0,  0,  2}, 2},
+            {{  7,  0,  0,  2}, 1},
+            {{ 10,  0,  0,  2}, 2},
+        };
+        arb::sample_tree sm(samples, parents);
+        arb::em_morphology em(arb::morphology(sm, false));
+
+        auto h1  = arb::reg::cable({0, 0, 0.5});
+        auto h2  = arb::reg::cable({0, 0.5, 1});
+        auto t1  = arb::reg::tagged(1);
+        auto t2  = arb::reg::tagged(2);
+        auto all = arb::reg::all();
+
+        // Concrete cable lists
+        cl h1_{{0, 0.0, 0.5}};
+        cl h2_{{0, 0.5, 1.0}};
+        cl t1_{{0, 0.0, 0.1}, {0, 0.3, 0.7}};
+        cl t2_{{0, 0.1, 0.3}, {0, 0.7, 1.0}};
+        cl all_{{0, 0, 1}};
+        cl empty_{};
+
+        EXPECT_EQ(thingify(h1, em), h1_);
+        EXPECT_EQ(thingify(h2, em), h2_);
+        EXPECT_EQ(thingify(t1, em), t1_);
+        EXPECT_EQ(thingify(t2, em), t2_);
+        EXPECT_EQ(thingify(join(h1, h2), em), all_);
+
+        EXPECT_EQ(thingify(intersect(h1, h2), em), (cl{{0, 0.5, 0.5}}));
+
+        EXPECT_EQ(thingify(intersect(h1, h1), em), h1_);
+        EXPECT_EQ(thingify(intersect(t1, t1), em), t1_);
+        EXPECT_EQ(thingify(join(t1, t2), em), all_);
+        EXPECT_EQ(thingify(intersect(all, t1), em), t1_);
+        EXPECT_EQ(thingify(intersect(all, t2), em), t2_);
+        EXPECT_EQ(thingify(join(all, t1), em), all_);
+        EXPECT_EQ(thingify(join(all, t2), em), all_);
+        EXPECT_EQ(thingify(join(h1, t1), em), (cl{{0, 0, 0.7}}));
+        EXPECT_EQ(thingify(join(h1, t2), em), (cl{{0, 0, 0.5}, {0, 0.7, 1}}));
+        EXPECT_EQ(thingify(intersect(h2, t1), em), (cl{{0, 0.5, 0.7}}));
+    }
+
+
+    // Test handling of spherical soma on multi-branch morphologies
+    //
+    //  sample ids:
+    //              0           |
+    //            1   3         |
+    //          2       4       |
+    //  tags:
+    //              1           |
+    //            3   2         |
+    //          3       2       |
+    {
+        pvec parents = {npos, 0, 1, 0, 3};
+        svec samples = {
+            {{  0,  0,  0,  2}, 1},
+            {{ 10,  0,  0,  2}, 3},
+            {{100,  0,  0,  2}, 3},
+            {{  0, 10,  0,  2}, 2},
+            {{  0,100,  0,  2}, 2},
+        };
+
+        // with a spherical root
+        arb::sample_tree sm(samples, parents);
+        arb::em_morphology em(arb::morphology(sm, true));
+
+        using arb::reg::tagged;
+        using arb::reg::branch;
+        using arb::reg::all;
+
+        EXPECT_EQ(thingify(tagged(1), em), (arb::mcable_list{{0,0,1}}));
+        EXPECT_EQ(thingify(tagged(2), em), (arb::mcable_list{{2,0,1}}));
+        EXPECT_EQ(thingify(tagged(3), em), (arb::mcable_list{{1,0,1}}));
+        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), em), (arb::mcable_list{{0,0,1}, {1,0,1}, {2,0,1}}));
+        EXPECT_EQ(thingify(join(tagged(1), tagged(2), tagged(3)), em), thingify(all(), em));
+    }
+
+    // Test multi-level morphologies.
+    //
+    // Eight samples
+    //
+    //  sample ids:
+    //            0
+    //           1 3
+    //          2   4
+    //             5 6
+    //                7
+    //  tags:
+    //            1
+    //           3 2
+    //          3   2
+    //             4 3
+    //                3
+    {
+        pvec parents = {npos, 0, 1, 0, 3, 4, 4, 6};
+        svec samples = {
+            {{  0,  0,  0,  2}, 1},
+            {{ 10,  0,  0,  2}, 3},
+            {{100,  0,  0,  2}, 3},
+            {{  0, 10,  0,  2}, 2},
+            {{  0,100,  0,  2}, 2},
+            {{100,100,  0,  2}, 4},
+            {{  0,200,  0,  2}, 3},
+            {{  0,300,  0,  2}, 3},
+        };
+        arb::sample_tree sm(samples, parents);
+
+        // Without spherical root
+        arb::em_morphology em(arb::morphology(sm, false));
+
+        using arb::reg::tagged;
+        using arb::reg::branch;
+        using arb::reg::all;
+        using arb::reg::cable;
+        using arb::mcable;
+        auto soma = tagged(1);
+        auto axon = tagged(2);
+        auto dend = tagged(3);
+        auto apic = tagged(4);
+        auto b1  = branch(1);
+        auto b3  = branch(3);
+        auto b13 = join(b1, b3);
+
+        cl empty_{};
+        cl soma_ = empty_;
+
+        // Whole branches:
+        mcable b0_{0,0,1};
+        mcable b1_{1,0,1};
+        mcable b2_{2,0,1};
+        mcable b3_{3,0,1};
+        cl all_  = {b0_,b1_,b2_,b3_};
+
+        mcable end1_{1,1,1};
+        mcable root_{0,0,0};
+
+        EXPECT_EQ(thingify(all(), em), all_);
+        EXPECT_EQ(thingify(soma, em), empty_);
+        EXPECT_EQ(thingify(axon, em), (cl{b1_}));
+        EXPECT_EQ(thingify(dend, em), (cl{b0_,b3_}));
+        EXPECT_EQ(thingify(apic, em), (cl{b2_}));
+        EXPECT_EQ(thingify(join(dend, apic), em), (cl{b0_,b2_,b3_}));
+        EXPECT_EQ(thingify(join(axon, join(dend, apic)), em), all_);
+
+        // Test that intersection correctly generates zero-length cables at
+        // parent-child interfaces.
+        EXPECT_EQ(thingify(intersect(apic, dend), em), (cl{end1_}));
+        EXPECT_EQ(thingify(intersect(apic, axon), em), (cl{end1_}));
+        EXPECT_EQ(thingify(intersect(axon, dend), em), (cl{root_, end1_}));
+
+        // Test some more interesting intersections and unions.
+
+        //    123456789 123456789
+        //   |---------|---------| lhs
+        //   |  -----  |   ---   | rhs
+        //   |  xxxxx  |   xxx   | rand
+        //   |xxxxxxxxx|xxxxxxxxx| ror
+        auto lhs  = b13;
+        auto rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        auto rand = cl{         {1,.2,.7}, {3,.3,.6}};
+        auto ror  = cl{         {1,.0,1.}, {3,.0,1.}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+
+        //    123456789 123456789
+        //   |   ----- | ----    | lhs
+        //   |  -----  |   ---   | rhs
+        //   |   xxxx  |   xx    | rand
+        //   |  xxxxxx | xxxxx   | ror
+        lhs  = join(cable({1,.3,.8}), cable({3,.1,.5}));
+        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        rand = cl{         {1,.3,.7}, {3,.3,.5}};
+        ror  = cl{         {1,.2,.8}, {3,.1,.6}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+
+        //    123456789 123456789
+        //   | -- -    | --- --- | lhs
+        //   |  -----  |   ---   | rhs
+        //   |  x x    |   x x   | rand
+        //   | xxxxxx  | xxxxxxx | ror
+        lhs  = join(cable({1,.1,.3}), cable({1,.4,.5}), cable({3,.1,.4}), cable({3,.5,.9}));
+        rhs  = join(cable({1,.2,.7}), cable({3,.3,.6}));
+        rand = cl{         {1,.2,.3}, {1,.4,.5}, {3,.3,.4}, {3,.5,.6}};
+        ror  = cl{         {1,.1,.7},            {3,.1,.9}};
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+
+        // Assert communtativity
+        std::swap(lhs, rhs);
+        EXPECT_EQ(thingify(intersect(lhs, rhs), em), rand);
+        EXPECT_EQ(thingify(join(lhs, rhs), em), ror);
+    }
+}
diff --git a/test/unit/test_morphology.cpp b/test/unit/test_morphology.cpp
index f0af3a4a80e78121496013f73f6009e892f0e90b..fddbd74434a83d13bf1981d9a664170c36b33f03 100644
--- a/test/unit/test_morphology.cpp
+++ b/test/unit/test_morphology.cpp
@@ -3,9 +3,7 @@
  */
 
 #include <fstream>
-#include <cassert>
 #include <cmath>
-#include <stdexcept>
 #include <string>
 #include <vector>
 
@@ -19,21 +17,38 @@
 
 #include "algorithms.hpp"
 #include "io/sepval.hpp"
+#include "morph/mbranch.hpp"
+#include "morph/em_morphology.hpp"
 #include "util/range.hpp"
 #include "util/span.hpp"
 #include "util/strprintf.hpp"
 
-// Forward declare non-public functions that are used internally to build
-// morphologies so that they can be tested.
-namespace arb { namespace impl {
-    std::vector<mbranch> branches_from_parent_index(const std::vector<arb::msize_t>&, const std::vector<point_prop>&, bool);
-}}
-
 template <typename T>
 std::ostream& operator<<(std::ostream& o, const std::vector<T>& v) {
     return o << "[" << arb::io::csv(v) << "]";
 }
 
+// Test basic functions on properties of mpoint
+TEST(morphology, mpoint) {
+    using mp = arb::mpoint;
+
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 0   , 0   }), 0.);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{3.14, 0   , 0   }), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 3.14, 0   }), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{0   , 0   , 3.14}), 3.14);
+    EXPECT_EQ(arb::distance(mp{0,0,0},mp{1   , 2   , 3   }), std::sqrt(14.));
+
+    EXPECT_TRUE(arb::is_collocated(mp{0,0,0}, mp{0,0,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{3,0,0}, mp{3,0,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{0,3,0}, mp{0,3,0}));
+    EXPECT_TRUE(arb::is_collocated(mp{0,0,3}, mp{0,0,3}));
+    EXPECT_TRUE(arb::is_collocated(mp{2,0,3}, mp{2,0,3}));
+
+    EXPECT_FALSE(arb::is_collocated(mp{1,0,3}, mp{2,0,3}));
+    EXPECT_FALSE(arb::is_collocated(mp{2,1,3}, mp{2,0,3}));
+    EXPECT_FALSE(arb::is_collocated(mp{2,0,1}, mp{2,0,3}));
+}
+
 TEST(morphology, point_props) {
     arb::point_prop p = arb::point_prop_mask_none;
 
@@ -142,7 +157,7 @@ TEST(sample_tree, properties) {
 TEST(morphology, branches_from_parent_index) {
     const auto npos = arb::mnpos;
     using pvec = std::vector<arb::msize_t>;
-    using mb = arb::mbranch;
+    using mb = arb::impl::mbranch;
 
     // make a sample tree from a parent vector with non-collocated points.
     auto make_tree = [] (const pvec& parents) {
@@ -555,7 +570,7 @@ TEST(morphology, swc) {
     EXPECT_EQ(31u, m.num_branches());
 
     // Confirm that converting to a cable_cell generates the same number of branches.
-    auto c = arb::make_cable_cell(m, false);
+    auto c = arb::make_cable_cell(m, {}, false);
     EXPECT_EQ(31u, c.num_segments());
 }
 
diff --git a/test/unit/test_probe.cpp b/test/unit/test_probe.cpp
index 989a6cc822cfd3617959e3adc7821c838c168f7b..5afba4f3e89a1f040eaf2e6764a4d58b49192464 100644
--- a/test/unit/test_probe.cpp
+++ b/test/unit/test_probe.cpp
@@ -28,9 +28,9 @@ TEST(probe, fvm_lowered_cell) {
 
     cable1d_recipe rec(bs);
 
-    segment_location loc0{0, 0};
-    segment_location loc1{1, 1};
-    segment_location loc2{1, 0.3};
+    mlocation loc0{0, 0};
+    mlocation loc1{1, 1};
+    mlocation loc2{1, 0.3};
 
     rec.add_probe(0, 10, cell_probe_address{loc0, cell_probe_address::membrane_voltage});
     rec.add_probe(0, 20, cell_probe_address{loc1, cell_probe_address::membrane_voltage});
diff --git a/test/unit/test_synapses.cpp b/test/unit/test_synapses.cpp
index 56700e8352fb9311bb6f8a740ba03ab678cda8c3..b7983f1ebadbb90924da1a461c3c1b52b2a68ee6 100644
--- a/test/unit/test_synapses.cpp
+++ b/test/unit/test_synapses.cpp
@@ -45,16 +45,16 @@ TEST(synapses, add_to_cell) {
     EXPECT_EQ(3u, cell.synapses().size());
     const auto& syns = cell.synapses();
 
-    EXPECT_EQ(syns[0].location.segment, 0u);
-    EXPECT_EQ(syns[0].location.position, 0.1);
+    EXPECT_EQ(syns[0].location.branch, 0u);
+    EXPECT_EQ(syns[0].location.pos, 0.1);
     EXPECT_EQ(syns[0].mechanism.name(), "expsyn");
 
-    EXPECT_EQ(syns[1].location.segment, 1u);
-    EXPECT_EQ(syns[1].location.position, 0.2);
+    EXPECT_EQ(syns[1].location.branch, 1u);
+    EXPECT_EQ(syns[1].location.pos, 0.2);
     EXPECT_EQ(syns[1].mechanism.name(), "exp2syn");
 
-    EXPECT_EQ(syns[2].location.segment, 0u);
-    EXPECT_EQ(syns[2].location.position, 0.3);
+    EXPECT_EQ(syns[2].location.branch, 0u);
+    EXPECT_EQ(syns[2].location.pos, 0.3);
     EXPECT_EQ(syns[2].mechanism.name(), "expsyn");
 }
 
diff --git a/test/unit/test_threading_exceptions.cpp b/test/unit/test_threading_exceptions.cpp
index c83896406c05b6acf76a07684b7e01ce11a70143..681a0ce4310783e3abd25bbcd6c4b66dfc9fe1ea 100644
--- a/test/unit/test_threading_exceptions.cpp
+++ b/test/unit/test_threading_exceptions.cpp
@@ -3,11 +3,7 @@
 #include <csignal>
 
 #include <arbor/domain_decomposition.hpp>
-#include <arbor/load_balance.hpp>
-#include <arbor/recipe.hpp>
-#include <arbor/simulation.hpp>
 #include <arbor/context.hpp>
-#include <arbor/cable_cell.hpp>
 #include <arbor/version.hpp>
 
 #include "threading/threading.hpp"
diff --git a/test/validation/validate_ball_and_stick.cpp b/test/validation/validate_ball_and_stick.cpp
index a7843adcb7983ae82790557fb30f070be2a64495..4f60de9e945b0b568d5d4fbb1cef29dbe32c563b 100644
--- a/test/validation/validate_ball_and_stick.cpp
+++ b/test/validation/validate_ball_and_stick.cpp
@@ -27,7 +27,7 @@ using namespace arb;
 
 struct probe_point {
     const char* label;
-    segment_location where;
+    mlocation where;
 };
 
 template <typename ProbePointSeq>