diff --git a/arbor/include/arbor/morph/locset.hpp b/arbor/include/arbor/morph/locset.hpp
index 24d9217efb7664d43ae60750fa923fb1cddbed89..a815b67f05c48294eaa7506e765aaa7f31e5f437 100644
--- a/arbor/include/arbor/morph/locset.hpp
+++ b/arbor/include/arbor/morph/locset.hpp
@@ -142,6 +142,9 @@ locset most_distal(region reg);
 // Most proximal point of a region.
 locset most_proximal(region reg);
 
+// Returns all locations in a locset that are also in the region.
+locset restrict(locset ls, region reg);
+
 // A range `left` to `right` of randomly selected locations with a
 // uniform distribution from region `reg` generated using `seed`
 locset uniform(region reg, unsigned left, unsigned right, uint64_t seed);
diff --git a/arbor/morph/locset.cpp b/arbor/morph/locset.cpp
index dc9b7617a00ff65134abc2d1f86933c01933ad54..0e4df9c32ed6dc358831e22656b541bae7f37101 100644
--- a/arbor/morph/locset.cpp
+++ b/arbor/morph/locset.cpp
@@ -348,6 +348,40 @@ std::ostream& operator<<(std::ostream& o, const lsum& x) {
     return o << "(sum " << x.lhs << " " << x.rhs << ")";
 }
 
+// Restrict a locset on to a region: returns all locations in the locset that
+// are also in the region.
+
+struct lrestrict_ {
+    locset ls;
+    region reg;
+};
+
+mlocation_list thingify_(const lrestrict_& P, const mprovider& p) {
+    mlocation_list L;
+
+    auto cables = thingify(P.reg, p).cables();
+    auto ends = util::transform_view(cables, [](const auto& c){return mlocation{c.branch, c.dist_pos};});
+
+    for (auto l: thingify(P.ls, p)) {
+        auto it = std::lower_bound(ends.begin(), ends.end(), l);
+        if (it==ends.end()) continue;
+        const auto& c = cables[std::distance(ends.begin(), it)];
+        if (c.branch==l.branch && c.prox_pos<=l.pos) {
+            L.push_back(l);
+        }
+    }
+
+    return L;
+}
+
+locset restrict(locset ls, region reg) {
+    return locset{lrestrict_{std::move(ls), std::move(reg)}};
+}
+
+std::ostream& operator<<(std::ostream& o, const lrestrict_& x) {
+    return o << "(restrict " << x.ls << " " << x.reg << ")";
+}
+
 } // namespace ls
 
 // The intersect and join operations in the arb:: namespace with locset so that
diff --git a/python/cells.cpp b/python/cells.cpp
index a74856bd58d46cd8ec0b70f0d19abe1c1cfc5741..e0ae99ac8b9c7af8b4d69e0345acd6e875218a5e 100644
--- a/python/cells.cpp
+++ b/python/cells.cpp
@@ -529,7 +529,7 @@ void register_cells(pybind11::module& m) {
         .def("locations",
             [](arb::cable_cell& c, const char* label) {return c.concrete_locset(label);},
             "label"_a, "The locations of the cell morphology for a locset label.")
-        .def("region",
+        .def("cables",
             [](arb::cable_cell& c, const char* label) {return c.concrete_region(label).cables();},
             "label"_a, "The cable segments of the cell morphology for a region label.")
         // Discretization control.
diff --git a/python/morph_parse.cpp b/python/morph_parse.cpp
index 38111045a0ee2cb11a8ff17e5653cd95e3927525..6c261029ced2b92d0715c0db9a5ed1bd37561a15 100644
--- a/python/morph_parse.cpp
+++ b/python/morph_parse.cpp
@@ -239,6 +239,8 @@ std::unordered_multimap<std::string, evaluator> eval_map {
                             "'on_branches' with 1 argument: (pos:double)")},
     {"locset",  make_call<std::string>(arb::ls::named,
                             "'locset' with 1 argument: (name:string)")},
+    {"restrict",  make_call<arb::locset, arb::region>(arb::ls::restrict,
+                            "'restrict' with 2 arguments: (ls:locset, reg:region)")},
     {"join",    make_fold<arb::locset>(static_cast<arb::locset(*)(arb::locset, arb::locset)>(arb::join),
                             "'join' with at least 2 arguments: (locset locset [...locset])")},
     {"sum",     make_fold<arb::locset>(static_cast<arb::locset(*)(arb::locset, arb::locset)>(arb::sum),
diff --git a/python/morphology.cpp b/python/morphology.cpp
index 4c2047d539bfb01e0a1c4073eae73d4634ad6895..443a1642de2dd0371f8918d5da234db46c4e8deb 100644
--- a/python/morphology.cpp
+++ b/python/morphology.cpp
@@ -38,7 +38,7 @@ void register_morphology(pybind11::module& m) {
             "  position: The relative position (from 0., proximal, to 1., distal) on the branch.\n")
         .def_readonly("branch",  &arb::mlocation::branch,
             "The id of the branch.")
-        .def_readonly("position", &arb::mlocation::pos,
+        .def_readonly("pos", &arb::mlocation::pos,
             "The relative position on the branch (∈ [0.,1.], where 0. means proximal and 1. distal).")
         .def("__str__",
             [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); })
@@ -101,9 +101,9 @@ void register_morphology(pybind11::module& m) {
                         return c;
                     }),
              "branch_id"_a, "prox"_a, "dist"_a)
-        .def_readonly("prox_pos", &arb::mcable::prox_pos,
+        .def_readonly("prox", &arb::mcable::prox_pos,
                 "The relative position of the proximal end of the cable on its branch ∈ [0,1].")
-        .def_readonly("dist_pos", &arb::mcable::dist_pos,
+        .def_readonly("dist", &arb::mcable::dist_pos,
                 "The relative position of the distal end of the cable on its branch ∈ [0,1].")
         .def_readonly("branch", &arb::mcable::branch,
                 "The id of the branch on which the cable lies.")
diff --git a/python/s_expr.cpp b/python/s_expr.cpp
index a7e40e8aa8298c1f583882fd86bcd41c45d4fea2..e879c17d1a02c7a81f8749e54950fd5d7fb85fcd 100644
--- a/python/s_expr.cpp
+++ b/python/s_expr.cpp
@@ -155,15 +155,17 @@ private:
     }
 
     // Parse alphanumeric sequence that starts with an alphabet character,
-    // and my contain alphabet, numeric or underscor '_' characters.
+    // and my contain alphabet, numeric or underscore '_' characters.
     //
     // Valid names:
     //    sub_dendrite
+    //    sub-dendrite
     //    temp_
     //    branch3
     //    A
     // Invalid names:
     //    _cat          ; can't start with underscore
+    //    -cat          ; can't start with hyphen
     //    2ndvar        ; can't start with numeric character
     //
     // Returns the appropriate token kind if name is a keyword.
@@ -182,7 +184,7 @@ private:
         while(1) {
             c = *current_;
 
-            if(is_alphanumeric(c) || c=='_') {
+            if(is_alphanumeric(c) || c=='_' || c=='-') {
                 name += character();
             }
             else {
diff --git a/python/test/cpp/s_expr.cpp b/python/test/cpp/s_expr.cpp
index 567ec754168b0c4eb07fc1816cbe35d998cf2aa4..772103e507a15b036b8ca515619d041ebae18d65 100644
--- a/python/test/cpp/s_expr.cpp
+++ b/python/test/cpp/s_expr.cpp
@@ -16,15 +16,21 @@ TEST(s_expr, identifier) {
     EXPECT_TRUE(test_identifier("f_1__"));
     EXPECT_TRUE(test_identifier("A_1__"));
 
+    EXPECT_TRUE(test_identifier("A-1"));
+    EXPECT_TRUE(test_identifier("hello-world"));
+    EXPECT_TRUE(test_identifier("hello--world"));
+    EXPECT_TRUE(test_identifier("hello--world_"));
+
     EXPECT_FALSE(test_identifier("_foobar"));
+    EXPECT_FALSE(test_identifier("-foobar"));
     EXPECT_FALSE(test_identifier("2dogs"));
     EXPECT_FALSE(test_identifier("1"));
     EXPECT_FALSE(test_identifier("_"));
+    EXPECT_FALSE(test_identifier("-"));
     EXPECT_FALSE(test_identifier(""));
     EXPECT_FALSE(test_identifier(" foo"));
     EXPECT_FALSE(test_identifier("foo "));
     EXPECT_FALSE(test_identifier("foo bar"));
-    EXPECT_FALSE(test_identifier("foo-bar"));
     EXPECT_FALSE(test_identifier(""));
 }
 
diff --git a/test/unit/morph_pred.hpp b/test/unit/morph_pred.hpp
index ebe57bd95bd9dbdb4450c51f87286d624f1e32da..c66e85cf9c5e96d30258b4a07a911244c73594a7 100644
--- a/test/unit/morph_pred.hpp
+++ b/test/unit/morph_pred.hpp
@@ -4,6 +4,7 @@
 
 #include "../gtest.h"
 
+#include <arbor/morph/locset.hpp>
 #include <arbor/morph/morphology.hpp>
 #include <arbor/morph/primitives.hpp>
 #include <arbor/morph/region.hpp>
@@ -69,7 +70,7 @@ inline ::testing::AssertionResult region_eq(const arb::mprovider& p, arb::region
 inline ::testing::AssertionResult mlocationlist_eq(const arb::mlocation_list& as, const arb::mlocation_list& bs) {
     if (as.size()!=bs.size()) {
         return ::testing::AssertionFailure()
-                << "cablelists " << as << " and " << bs << " differ";
+                << "mlocation lists " << as << " and " << bs << " differ";
     }
 
     for (auto i: arb::util::count_along(as)) {
@@ -80,5 +81,10 @@ inline ::testing::AssertionResult mlocationlist_eq(const arb::mlocation_list& as
     return ::testing::AssertionSuccess();
 }
 
+inline ::testing::AssertionResult locset_eq(const arb::mprovider& p, arb::locset a, arb::locset b) {
+    return mlocationlist_eq(thingify(a, p), thingify(b, p));
+}
+
+
 } // namespace testing
 
diff --git a/test/unit/test_morph_expr.cpp b/test/unit/test_morph_expr.cpp
index 35b0db1c25d2cc14b36388d7047a9baca9f5d896..af9ef3c697902f57956d2bfac045c9cbd2caf954 100644
--- a/test/unit/test_morph_expr.cpp
+++ b/test/unit/test_morph_expr.cpp
@@ -20,6 +20,7 @@ using namespace arb;
 using embedding = embed_pwlin;
 
 using testing::region_eq;
+using testing::locset_eq;
 using testing::cablelist_eq;
 using testing::mlocationlist_eq;
 
@@ -574,6 +575,42 @@ TEST(region, thingify_moderate_morphologies) {
         EXPECT_TRUE(region_eq(mp, radius_gt(reg_c_, 2), cl{{0,0.55,0.7},{2,0,0.5},{3,0.1,0.375},{3,0.9,1}}));
         EXPECT_TRUE(region_eq(mp, radius_gt(reg_d_, 2), cl{{0,0.55,0.7},{2,0,0.5},{3,0.1,0.375},{3,0.75,0.9}}));
 
+        // Test restriction
+
+        {
+            using ll = mlocation_list;
+            // two empty inputs -> empty output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(ll{}, {}),                  ll{}));
+            // empty locset + non-empty region -> empty output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(ll{}, reg::all()),          ll{}));
+            // non-empty locset + empty region -> empty output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(ll{{0,0.4}, {3, 0.1}}, {}), ll{}));
+
+            ll locs{{0,0.4}, {3,0.1}};
+            // none of locs in region -> empty output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(locs, branch(1)), ll{}));
+            // some but not all locs in region -> correct subset
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(locs, branch(0)), ll{{0,0.4}}));
+            // all locs in region -> locs in output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(locs, join(branch(0), branch(3))), locs));
+            // all locs in region -> locs in output
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(locs, join(branch(0), branch(1), branch(3))), locs));
+            // should also work with non-ordered input locset
+            std::reverse(locs.begin(), locs.end());
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(locs, join(branch(0), branch(3))), locs));
+
+            mlocation loc{1,0.5};
+            // location at end of cable
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(loc, cable(1, 0.2, 0.5)), loc));
+            // location at start of cable
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(loc, cable(1, 0.5, 0.7)), loc));
+            // location in zero-length cable
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(loc, cable(1, 0.5, 0.5)), loc));
+            // location between cable end points
+            EXPECT_TRUE(locset_eq(mp, ls::restrict(loc, cable(1, 0.2, 0.7)), loc));
+        }
+
+
         // Test some more interesting intersections and unions.
 
         //    123456789 123456789