IPSpace fixes.
diff --git a/swoc++/include/swoc/DiscreteRange.h b/swoc++/include/swoc/DiscreteRange.h
index 6838b14..f98f7db 100644
--- a/swoc++/include/swoc/DiscreteRange.h
+++ b/swoc++/include/swoc/DiscreteRange.h
@@ -1,4 +1,5 @@
 #pragma once
+
 // SPDX-License-Identifier: Apache-2.0
 // Copyright 2014 Network Geographics
 
@@ -64,7 +65,7 @@
 } // namespace detail
 
 /// Relationship between two intervals.
-enum DiscreteRangeRelation {
+enum class DiscreteRangeRelation : uint8_t {
   NONE,     ///< No common elements.
   EQUAL,    ///< Identical ranges.
   SUBSET,   ///< All elements in LHS are also in RHS.
@@ -73,6 +74,14 @@
   ADJACENT  ///< The two intervals are adjacent and disjoint.
 };
 
+/// Relationship between one edge of an interval and the "opposite" edge of another.
+enum class DiscreteRangeEdgeRelation : uint8_t  {
+  NONE, ///< Edge is on the opposite side of the relating edge.
+  GAP, ///< There is a gap between the edges.
+  ADJ, ///< The edges are adjacent.
+  OVLP, ///< Edge is inside interval.
+};
+
 /** A range over a discrete finite value metric.
    @tparam T The type for the range values.
 
@@ -100,6 +109,7 @@
 public:
   using metric_type = T;
   using Relation = DiscreteRangeRelation;
+  using EdgeRelation = DiscreteRangeEdgeRelation;
 
 //  static constexpr self_type ALL{detail::minimum<metric_type>(), detail::maximum<metric_type>()};
 
@@ -113,7 +123,7 @@
    *
    * @note Not marked @c explicit and so serves as a conversion from scalar values to an interval.
    */
-  constexpr DiscreteRange(T const &value) : _min(value), _max(value){};
+  constexpr DiscreteRange(T const &value) : _min(value), _max(value) {};
 
   /** Constructor.
    *
@@ -173,6 +183,13 @@
    */
   bool is_adjacent_to(self_type const &that) const;
 
+  /** Test for @a this being adjacent on the left of @a that.
+   *
+   * @param that Range to check for adjacency.
+   * @return @c true if @a this ends exactly the value before @a that begins.
+   */
+  bool is_left_adjacent_to(self_type const& that) const;
+
   //! Test if the union of two intervals is also an interval.
   bool has_union(self_type const &that) const;
 
@@ -201,6 +218,27 @@
    */
   Relation relationship(self_type const &that) const;
 
+  /** Determine the relationship of the left edge of @a that with @a this.
+   *
+   * @param that The other interval.
+   * @return The edge relationship.
+   *
+   * This checks the right edge of @a this against the left edge of @a that.
+   *
+   * - GAP: @a that left edge is right of @a this.
+   * - ADJ: @a that left edge is right adjacent to @a this.
+   * - OVLP: @a that left edge is inside @a this.
+   * - NONE: @a that left edge is left of @a this.
+   */
+  EdgeRelation left_edge_relationship(self_type const& that) const {
+    if (_max < that._max) {
+      auto tmp{_max};
+      ++tmp;
+      return tmp < that._max ? EdgeRelation::GAP : EdgeRelation::ADJ;
+    }
+    return _min >= that._min ? EdgeRelation::NONE : EdgeRelation::OVLP;
+  }
+
   /** Compute the convex hull of this interval and another one.
       @return The smallest interval that is a superset of @c this
       and @a that interval.
@@ -287,18 +325,18 @@
 template <typename T>
 typename DiscreteRange<T>::Relation
 DiscreteRange<T>::relationship(self_type const &that) const {
-  Relation retval = NONE;
+  Relation retval = Relation::NONE;
   if (this->has_intersection(that)) {
     if (*this == that)
-      retval = EQUAL;
+      retval = Relation::EQUAL;
     else if (this->is_subset_of(that))
-      retval = SUBSET;
+      retval = Relation::SUBSET;
     else if (this->is_superset_of(that))
-      retval = SUPERSET;
+      retval = Relation::SUPERSET;
     else
-      retval = OVERLAP;
+      retval = Relation::OVERLAP;
   } else if (this->is_adjacent_to(that)) {
-    retval = ADJACENT;
+    retval = Relation::ADJACENT;
   }
   return retval;
 }
@@ -417,6 +455,12 @@
 template <typename T>
 bool
 DiscreteRange<T>::is_adjacent_to(DiscreteRange::self_type const &that) const {
+  return this->is_left_adjacent_to(that) || that.is_left_adjacent_to(*this);
+}
+
+template <typename T>
+bool
+DiscreteRange<T>::is_left_adjacent_to(DiscreteRange::self_type const &that) const {
   /* Need to be careful here. We don't know much about T and we certainly don't know if "t+1"
    * even compiles for T. We do require the increment operator, however, so we can use that on a
    * copy to get the equivalent of t+1 for adjacency testing. We must also handle the possibility
@@ -427,9 +471,6 @@
   if (_max < that._min) {
     T x(_max);
     return ++x == that._min;
-  } else if (that._max < _min) {
-    T x(that._max);
-    return ++x == _min;
   }
   return false;
 }
@@ -578,6 +619,8 @@
      */
     self_type & assign(PAYLOAD const &payload);
 
+    range_type const& range() const { return _range; }
+
     self_type &
     assign_min(METRIC const &m) {
       _range.assign_min(m);
@@ -1167,16 +1210,15 @@
   // Used to hold a temporary blended node - @c release if put in space, otherwise cleaned up.
   using unique_node = std::unique_ptr<Node, decltype(node_cleaner)>;
 
-  // Rightmost node of interest with n->_min <= min.
+  // Rightmost node of interest with n->min() <= range.min().
   Node *n = this->lower_bound(range.min());
-  Node *pred = nullptr;
 
   // This doesn't change, compute outside loop.
   auto range_max_plus_1 = range.max();
   ++range_max_plus_1; // only use in contexts where @a max < METRIC max value.
 
-  // Update every loop to be the place to start blending.
-  auto range_min = range.min();
+  // Update every loop to track what remains to be filled.
+  auto remaining = range;
 
   if (nullptr == n) {
     n = this->head();
@@ -1184,61 +1226,75 @@
 
   // Process @a n, covering the values from the previous range to @a n.max
   while (n) {
-    // min is the smallest value of interest, need to fill from there on to the right.
-    metric_type min, min_minus_1;
-
-    pred = n ? prev(n) : nullptr;
-    if (pred && pred->max() >= range.min()) {
-      min_minus_1 = min = pred->max();
-      ++min;
-    } else {
-      min_minus_1 = min = range.min();
-      --min_minus_1;
-      if (pred && pred->max() < min_minus_1) {
-        pred = nullptr;
-      }
+    // Always look back at prev, so if there's no overlap at all, skip it.
+    if (n->max() < remaining.min()) {
+      n = next(n);
+      continue;
     }
-    // if @a pred is set, then it's adjacent or overlapping on the left.
 
-    // Do some important computations once, caching the results.
-    // @a n extends past @a range, so the trailing segment must be dealt with.
-    bool right_ext_p = n->max() > range.max();
-    // @a n overlaps with @a range, but only to the right.
-    bool right_overlap_p = n->min() <= range.max() && n->min() >= range.min();
-    // @a n is adjacent on the right to @a range.
-    bool right_adj_p = !right_overlap_p && n->min() == range_max_plus_1;
+    // Invariant - n->max() >= remaining.min();
+    Node* pred = prev(n);
+
+    // Check for left extension. If found, clip that node to be adjacent and put in a
+    // temporary that covers the overlap with the original payload.
+    if (n->min() < remaining.min()) {
+      auto stub = _fa.make(remaining.min(), n->max(), n->payload());
+      auto x { remaining.min() };
+      --x;
+      n->assign_max(x);
+      this->insert_after(n, stub);
+      pred = n;
+      n = stub;
+    }
+
+    auto pred_edge = pred ? remaining.left_edge_relationship(pred->range()) : DiscreteRangeEdgeRelation::NONE;
+    // invariant - pred->max() < remaining.min()
+
+    // Calculate and cache key relationships between @a n and @a remaining.
+
+    // @a n extends past @a remaining, so the trailing segment must be dealt with.
+    bool right_ext_p = n->max() > remaining.max();
+    // @a n strictly right overlaps with @a remaining.
+    bool right_overlap_p = remaining.contains(n->min());
+    // @a n is adjacent on the right to @a remaining.
+    bool right_adj_p = remaining.is_left_adjacent_to(n->range());
     // @a n has the same color as would be used for unmapped values.
     bool n_plain_colored_p = plain_color_p && (n->payload() == plain_color);
-    // @a pred has the same color as would be used for unmapped values.
-    bool pred_plain_colored_p = plain_color_p && pred && pred->payload() == plain_color;
+    // @a rped has the same color as would be used for unmapped values.
+    bool pred_plain_colored_p =
+        (DiscreteRangeEdgeRelation::NONE != pred_edge && DiscreteRangeEdgeRelation::GAP != pred_edge) &&
+        pred->payload() == plain_color;
 
-    // Check for no right overlap - that means the next node is past the target range.
+    // Check for no right overlap - that means @a n is past the target range.
+    // It may be possible to extend @a n or the previous range to cover
+    // the target range. Regardless, all of @a range can be filled at this point.
     if (!right_overlap_p) {
       if (right_adj_p && n_plain_colored_p) { // can pull @a n left to cover
-        n->assign_min(min);
+        n->assign_min(remaining.min());
         if (pred_plain_colored_p) { // if that touches @a pred with same color, collapse.
           n->assign_min(pred->min());
           this->remove(pred);
         }
       } else if (pred_plain_colored_p) { // can pull @a pred right to cover.
-        pred->assign_max(range.max());
-      } else { // Must add new range.
-        this->insert_after(n, _fa.make(min, range.max(), plain_color));
+        pred->assign_max(remaining.max());
+      } else if (! remaining.is_empty()) { // Must add new range.
+        this->insert_before(n, _fa.make(remaining.min(), remaining.max(), plain_color));
       }
       return *this;
     }
 
-    // Invariant: There is overlap between @a n and @a range.
+    // Invariant: @n has right overlap with @a remaining
 
-    // Fill from @a min to @a n.min - 1
-    if (plain_color_p && min < n->min()) { // can fill and there's space to fill.
+    // If there's a gap on the left, fill from @a min to @a n.min - 1
+    // Also see above - @a pred is set iff it is left overlapping or left adjacent.
+    if (plain_color_p && remaining.min() < n->min()) {
       if (n->payload() == plain_color) {
         if (pred && pred->payload() == n->payload()) {
           auto pred_min{pred->min()};
           this->remove(pred);
           n->assign_min(pred_min);
         } else {
-          n->assign_min(min);
+          n->assign_min(remaining.min());
         }
       } else {
         auto n_min_minus_1{n->min()};
@@ -1246,18 +1302,19 @@
         if (pred && pred->payload() == plain_color) {
           pred->assign_max(n_min_minus_1);
         } else {
-          this->insert_before(n, _fa.make(min, n_min_minus_1, plain_color));
+          this->insert_before(n, _fa.make(range.min(), n_min_minus_1, plain_color));
         }
       }
     }
 
+    // Invariant: Space in @a range and to the left of @a n has been filled.
+
     // Create a node with the blend for the overlap and then update / replace @a n as needed.
-    auto max { right_ext_p ? range.max() : n->max() }; // smallest boundary of range and @a n.
+    auto max { right_ext_p ? remaining.max() : n->max() }; // smallest boundary of range and @a n.
     unique_node fill { _fa.make(n->min(), max, n->payload()), node_cleaner };
     bool fill_p = blender(fill->payload(), color); // fill or clear?
     auto next_n = next(n); // cache this in case @a n is removed.
-    range_min = fill->max(); // blend will be updated to one past @a fill
-    ++range_min;
+    remaining.assign_min(++METRIC{fill->max()}); // Update what is left to fill.
 
     // Clean up the range for @a n
     if (fill_p) {
@@ -1270,10 +1327,8 @@
           return *this;
         }
       } else {
-        // PROBLEM - not collapsing into @a pred(n) when it's adjacent / matching color.
-        // But @a pred may have been removed above, not reliable at this point.
-        pred = prev(n);
-        if (pred && pred->payload() == fill->payload()) {
+        // Collapse in to previous range if it's adjacent and the color matches.
+        if (nullptr != (pred = prev(n)) && pred->range().is_left_adjacent_to(fill->range()) && pred->payload() == fill->payload()) {
           this->remove(n);
           pred->assign_max(fill->max());
         } else {
@@ -1281,13 +1336,11 @@
           this->remove(n);
         }
       }
+    } else if (right_ext_p) {
+      n->assign_min(range_max_plus_1);
+      return *this;
     } else {
-      if (right_ext_p) {
-        n->assign_min(range_max_plus_1);
-        return *this;
-      } else {
-        this->remove(n);
-      }
+      this->remove(n);
     }
 
     // Everything up to @a n.max is correct, time to process next node.
@@ -1296,14 +1349,14 @@
 
   // Arriving here means there are no more ranges past @a range (those cases return from the loop).
   // Therefore the final fill node is always last in the tree.
-  if (plain_color_p && range_min <= range.max()) {
+  if (plain_color_p && remaining.min() <= range.max()) {
     // Check if the last node can be extended to cover because it's left adjacent.
     // Can decrement @a range_min because if there's a range to the left, @a range_min is not minimal.
     n = _list.tail();
-    if (n && n->max() >= --range_min && n->payload() == plain_color) {
+    if (n && n->max() >= --METRIC{remaining.min()} && n->payload() == plain_color) {
       n->assign_max(range.max());
     } else {
-      this->append(_fa.make(range_min, range.max(), plain_color));
+      this->append(_fa.make(remaining.min(), remaining.max(), plain_color));
     }
   }
 
diff --git a/swoc++/include/swoc/IntrusiveDList.h b/swoc++/include/swoc/IntrusiveDList.h
index 4d69aa9..f91079c 100644
--- a/swoc++/include/swoc/IntrusiveDList.h
+++ b/swoc++/include/swoc/IntrusiveDList.h
@@ -148,10 +148,31 @@
     /// Inequality
     bool operator!=(self_type const &that) const;
 
+    /// Check if @c std::prev would be valid.
+    /// @return @c true if calling @c std::prev would yield a valid iterator.
+    /// @note Identical to @c has_predecessor.
+    bool has_prev() const;
+
+    /// Check if @c std::next would be valid.
+    /// @return @c true if calling @c std::next would yield a valid iterator.
+    bool has_next() const;
+
+    /// Check if there is a predecessor.
+    /// @return @c true if there is a predecessor element for the current element.
+    /// @note Identical to @c has_prev.
+    bool has_predecessor() const;
+
+    /// Check if there is a successor.
+    /// @return @c true if after incrementing, the iterator will reference a value.
+    /// @note This is subtly different from @c has_net. It is false for the last element in the
+    /// iteration. Even though incrementing such an iterator would valid, there would not be a
+    /// successor element.
+    bool has_successor() const;
+
   protected:
     // These are stored non-const to make implementing @c iterator easier. This class provides the required @c const
     // protection.
-    list_type *_list{nullptr};                   ///< Needed to descrement from @c end() position.
+    list_type *_list{nullptr};                   ///< Needed to decrement from @c end() position.
     typename list_type::value_type *_v{nullptr}; ///< Referenced element.
 
     /// Internal constructor for containers.
@@ -579,6 +600,26 @@
   return this->_v != that._v;
 }
 
+template<typename L>
+bool IntrusiveDList<L>::const_iterator::has_predecessor() const {
+  return _v ? nullptr != L::prev_ptr(_v) : ! _list->empty();
+}
+
+template<typename L>
+bool IntrusiveDList<L>::const_iterator::has_prev() const {
+  return _v ? nullptr != L::prev_ptr(_v) : ! _list->empty();
+}
+
+template<typename L>
+bool IntrusiveDList<L>::const_iterator::has_successor() const {
+  return _v && nullptr != L::next_ptr(_v);
+}
+
+template<typename L>
+bool IntrusiveDList<L>::const_iterator::has_next() const {
+  return nullptr != _v;
+}
+
 template <typename L>
 auto
 IntrusiveDList<L>::prepend(value_type *v) -> self_type & {
diff --git a/swoc++/include/swoc/bwf_ip.h b/swoc++/include/swoc/bwf_ip.h
index 2b19d7f..63906d8 100644
--- a/swoc++/include/swoc/bwf_ip.h
+++ b/swoc++/include/swoc/bwf_ip.h
@@ -20,6 +20,7 @@
 
 #pragma once
 
+#include <iosfwd>
 #include "swoc/bwf_base.h"
 #include "swoc/swoc_ip.h"
 #include <netinet/in.h>
@@ -29,13 +30,60 @@
 // All of these expect the address to be in network order.
 BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, sockaddr const *addr);
 BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, in6_addr const &addr);
-BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IP4Addr const &addr);
-BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IPAddr const &addr);
 BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, sockaddr const *addr);
+// Use class information for ordering.
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IP4Addr const &addr);
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IP6Addr const &addr);
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IPAddr const &addr);
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IP4Range const &Range);
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IP6Range const &Range);
+BufferWriter &bwformat(BufferWriter &w, bwf::Spec const &spec, IPRange const &Range);
 
 inline BufferWriter &
 bwformat(BufferWriter &w, bwf::Spec const &spec, IPEndpoint const &addr) {
   return bwformat(w, spec, &addr.sa);
 }
 
+/// Buffer space sufficient for printing any basic IP address type.
+static const size_t IP_STREAM_SIZE = 80;
+
 } // namespace swoc
+
+namespace std {
+inline ostream &
+operator<<(ostream &s, swoc::IP4Addr const &addr) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, addr);
+}
+
+inline ostream &
+operator<<(ostream &s, swoc::IP6Addr const &addr) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, addr);
+}
+
+inline ostream &
+operator<<(ostream &s, swoc::IPAddr const &addr) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, addr);
+}
+
+inline ostream &
+operator<<(ostream &s, swoc::IP4Range const &Range) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, Range);
+}
+
+inline ostream &
+operator<<(ostream &s, swoc::IP6Range const &Range) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, Range);
+}
+
+inline ostream &
+operator<<(ostream &s, swoc::IPRange const &Range) {
+  swoc::LocalBufferWriter<swoc::IP_STREAM_SIZE> w;
+  return s << bwformat(w, swoc::bwf::Spec::DEFAULT, Range);
+}
+} // namespace std
+
diff --git a/swoc++/include/swoc/swoc_ip.h b/swoc++/include/swoc/swoc_ip.h
index 66213cb..ebd365a 100644
--- a/swoc++/include/swoc/swoc_ip.h
+++ b/swoc++/include/swoc/swoc_ip.h
@@ -8,9 +8,9 @@
 #include <string_view>
 #include <variant>
 
+#include <swoc/TextView.h>
 #include <swoc/DiscreteRange.h>
 #include <swoc/RBTree.h>
-#include "bwf_base.h"
 
 namespace swoc
 {
@@ -121,7 +121,7 @@
   /// @return This object.
   self_type &set_to_any(int family);
 
-  /// Set to be loopback for family @a family.
+  /// Set to be loopback address for family @a family.
   /// @a family must be @c AF_INET or @c AF_INET6.
   /// @return This object.
   self_type &set_to_loopback(int family);
@@ -161,30 +161,49 @@
 
   constexpr IP4Addr() = default; ///< Default constructor - invalid result.
 
-  /// Construct using IPv4 @a addr.
+  /// Construct using IPv4 @a addr (in host order).
+  /// @note Host order seems odd, but all of the standard network macro values such as @c INADDR_LOOPBACK
+  /// are in host order.
   explicit constexpr IP4Addr(in_addr_t addr);
   /// Construct from @c sockaddr_in.
-  explicit IP4Addr(sockaddr_in const *addr);
+  explicit IP4Addr(sockaddr_in const *sa);
   /// Construct from text representation.
   /// If the @a text is invalid the result is an invalid instance.
   IP4Addr(string_view const &text);
+  /// Construct from generic address @a addr.
+  explicit IP4Addr(IPAddr const& addr);
 
   /// Assign from IPv4 raw address.
   self_type &operator=(in_addr_t ip);
   /// Set to the address in @a addr.
-  self_type &operator=(sockaddr_in const *addr);
+  self_type &operator=(sockaddr_in const *sa);
 
+  /// Increment address.
   self_type &operator++();
 
+  /// Decrement address.
   self_type &operator--();
 
+  /** Byte access.
+   *
+   * @param idx Byte index.
+   * @return The byte at @a idx in the address.
+   */
+  uint8_t operator [] (unsigned idx) const {
+    return reinterpret_cast<bytes const&>(_addr)[idx];
+  }
+
+  /// Apply @a mask to address, leaving the network portion.
   self_type &operator&=(IPMask const& mask);
+  /// Apply @a mask to address, creating the broadcast address.
   self_type &operator|=(IPMask const& mask);
 
-  /// Write to @c sockaddr.
-  sockaddr *fill(sockaddr_in *sa, in_port_t port = 0) const;
+  /// Write this adddress and @a port to the sockaddr @a sa.
+  sockaddr_in *fill(sockaddr_in *sa, in_port_t port = 0) const;
 
+  /// @return The address in network order.
   in_addr_t network_order() const;
+  /// @return The address in host order.
   in_addr_t host_order() const;
 
   /** Parse @a text as IPv4 address.
@@ -200,22 +219,28 @@
 
   /// Get the IP address family.
   /// @return @c AF_INET
-  sa_family_t family() const;
-
-  /// Conversion to base type.
-  operator in_addr_t() const;
+  sa_family_t family() const { return AF_INET; }
 
   /// Test for multicast
-  bool is_multicast() const;
+  bool is_multicast() const { return IN_MULTICAST(_addr); }
 
   /// Test for loopback
-  bool is_loopback() const;
+  bool is_loopback() const { return (*this)[0] == IN_LOOPBACKNET; }
 
-  constexpr static in_addr_t reorder(in_addr_t src) {
-    return ((src & 0xFF) << 24) | (((src >> 8) & 0xFF) << 16) | (((src >> 16) & 0xFF) << 8) | ((src >> 24) & 0xFF);
-  }
+  /** Convert between network and host order.
+   *
+   * @param src Input address.
+   * @return @a src with the byte reversed.
+   *
+   * This performs the same computation as @c ntohl and @c htonl but is @c constexpr to be usable
+   * in situations those two functions are not.
+   */
+  constexpr static in_addr_t reorder(in_addr_t src);
 
 protected:
+  /// Access by bytes.
+  using bytes = std::array<uint8_t, 4>;
+
   friend bool operator==(self_type const &, self_type const &);
   friend bool operator!=(self_type const &, self_type const &);
   friend bool operator<(self_type const &, self_type const &);
@@ -236,12 +261,16 @@
   static constexpr size_t WORD_SIZE = sizeof(uint64_t); ///< Size of words used to store address.
   static constexpr size_t N_QUADS = SIZE / sizeof(QUAD); ///< # of quads in an IPv6 address.
 
-  /// Type for the actual address.
+  /// Direct access type for the address.
   /// Equivalent to the data type for data member @c s6_addr in @c in6_addr.
   using raw_type = std::array<unsigned char, SIZE>;
+  /// Direct access type for the address by quads (16 bits).
+  /// This corresponds to the elements of the text format of the address.
   using quad_type = std::array<unsigned short, N_QUADS>;
 
+  /// Minimum value of an address.
   static const self_type MIN;
+  /// Maximum value of an address.
   static const self_type MAX;
 
   IP6Addr() = default; ///< Default constructor - 0 address.
@@ -256,8 +285,13 @@
   /// If the @a text is invalid the result is an invalid instance.
   IP6Addr(string_view const& text);
 
+  /// Construct from generic @a addr.
+  IP6Addr(IPAddr const& addr);
+
+  /// Increment address.
   self_type &operator++();
 
+  /// Decrement address.
   self_type &operator--();
 
   /// Assign from IPv6 raw address.
@@ -271,6 +305,7 @@
   /// Copy address to @a addr in network order.
   in6_addr & copy_to(in6_addr & addr) const;
 
+  /// Return the address in network order.
   in6_addr network_order() const;
 
   /** Parse a string for an IP address.
@@ -287,13 +322,13 @@
 
   /// Get the address family.
   /// @return The address family.
-  sa_family_t family() const;
+  sa_family_t family() const { return AF_INET6; }
 
   /// Test for multicast
-  bool is_multicast() const;
+  bool is_loopback() const { return IN6_IS_ADDR_LOOPBACK(_addr._raw.data()); }
 
   /// Test for loopback
-  bool is_loopback() const;
+  bool is_multicast() const { return IN6_IS_ADDR_MULTICAST(_addr._raw.data()); }
 
   self_type & clear() {
     _addr._u64[0] = _addr._u64[1] = 0;
@@ -309,14 +344,23 @@
   friend bool operator!=(self_type const &, self_type const &);
   friend bool operator<(self_type const &, self_type const &);
   friend bool operator<=(self_type const &, self_type const &);
+
+  /// Type for digging around inside the address, with the various forms of access.
   union {
     uint64_t _u64[2] = {0, 0}; ///< 0 is MSW, 1 is LSW.
     quad_type _quad; ///< By quad.
     raw_type _raw; ///< By byte.
   } _addr;
 
+  /// Index of quads in @a _addr._quad.
+  /// This converts from the position in the text format to the quads in the binary format.
   static constexpr std::array<unsigned, N_QUADS> QUAD_IDX = { 3,2,1,0,7,6,5,4 };
 
+  /** Construct from two 64 bit values.
+   *
+   * @param msw The most significant 64 bits, host order.
+   * @param lsw The least significant 64 bits, host order.
+   */
   IP6Addr(uint64_t msw, uint64_t lsw) : _addr{msw, lsw} {}
 };
 
@@ -343,7 +387,7 @@
   explicit IPAddr(IPEndpoint const &addr);
   /// Construct from text representation.
   /// If the @a text is invalid the result is an invalid instance.
-  explicit IPAddr(string_view text);
+  explicit IPAddr(string_view const& text);
 
   /// Set to the address in @a addr.
   self_type &assign(sockaddr const *addr);
@@ -396,6 +440,10 @@
   in_addr_t network_ip4() const;
   in6_addr network_ip6() const;
 
+  explicit operator IP4Addr const&() const { return _addr._ip4; }
+
+  explicit operator IP6Addr const&() const { return _addr._ip6; }
+
   /// Test for validity.
   bool is_valid() const;
 
@@ -413,6 +461,8 @@
 
 protected:
   friend bool operator==(self_type const &, self_type const &);
+  friend IP4Addr;
+  friend IP6Addr;
 
   /// Address data.
   union raw_addr_type {
@@ -457,7 +507,7 @@
   raw_type width() const;
 
   /// Family type.
-  sa_family_t family() const;
+  sa_family_t family() const { return _family; }
 
   /// Write the mask as an address to @a addr.
   /// @return The filled address.
@@ -479,9 +529,16 @@
 public:
   using super_type::super_type; ///< Import super class constructors.
 
+  /// Default constructor, invalid range.
   IP4Range() = default;
+
+  /// Construct from an network expressed as @a addr and @a mask.
   IP4Range(IP4Addr const& addr, IPMask const& mask);
 
+  /// Construct from super type.
+  /// @internal Why do I have to do this, even though the super type constructors are inherited?
+  IP4Range(super_type const& r) : super_type(r) {}
+
   /** Construct range from @a text.
    *
    * @param text Range text.
@@ -490,18 +547,24 @@
    * This results in a zero address if @a text is not a valid string. If this should be checked,
    * use @c load.
    */
-  IP4Range(string_view text) {
-    this->load(text);
-  }
+  IP4Range(string_view const& text);
 
+  /** Set @a this range.
+   *
+   * @param addr Minimum address.
+   * @param mask CIDR mask to compute maximum adddress from @a addr.
+   * @return @a this
+   */
   self_type & assign(IP4Addr const& addr, IPMask const& mask);
 
   /** Assign to this range from text.
+   *
+   * @param text Range text.
+   *
    * The text must be in one of three formats.
    * - A dashed range, "addr1-addr2"
    * - A singleton, "addr". This is treated as if it were "addr-addr", a range of size 1.
    * - CIDR notation, "addr/cidr" where "cidr" is a number from 0 to the number of bits in the address.
-   * @param text Range text.
    */
   bool load(string_view text);
 
@@ -515,8 +578,27 @@
 public:
   using super_type::super_type; ///< Import super class constructors.
 
+  /// Construct from super type.
+  /// @internal Why do I have to do this, even though the super type constructors are inherited?
+  IP6Range(super_type const& r) : super_type(r) {}
+
+  /** Set @a this range.
+   *
+   * @param addr Minimum address.
+   * @param mask CIDR mask to compute maximum adddress from @a addr.
+   * @return @a this
+   */
   self_type & assign(IP6Addr const& addr, IPMask const& mask);
 
+  /** Assign to this range from text.
+   *
+   * @param text Range text.
+   *
+   * The text must be in one of three formats.
+   * - A dashed range, "addr1-addr2"
+   * - A singleton, "addr". This is treated as if it were "addr-addr", a range of size 1.
+   * - CIDR notation, "addr/cidr" where "cidr" is a number from 0 to the number of bits in the address.
+   */
   bool load(string_view text);
 
 };
@@ -524,11 +606,26 @@
 class IPRange {
   using self_type = IPRange;
 public:
+  /// Default constructor - construct invalid range.
   IPRange() = default;
+  /// Construct from an IPv4 @a range.
+  IPRange(IP4Range const& range);
+  /// Construct from an IPv6 @a range.
+  IPRange(IP6Range const& range);
+  /** Construct from a string format.
+   *
+   * @param text Text form of range.
+   *
+   * The string can be a single address, two addresses separated by a dash '-' or a CIDR network.
+   */
+  IPRange(string_view const& text);
 
-  bool is(sa_family_t f) const {
-    return f == _family;
-  }
+  /** Check if @a this range is the IP address @a family.
+   *
+   * @param family IP address family.
+   * @return @c true if this is @a family, @c false if not.
+   */
+  bool is(sa_family_t family) const ;
 
   /** Load the range from @a text.
    *
@@ -540,7 +637,9 @@
    */
   bool load(std::string_view const& text);
 
+  /// @return The minimum address in the range.
   IPAddr min() const;
+  /// @return The maximum address in the range.
   IPAddr max() const;
 
   operator IP4Range & () { return _range._ip4; }
@@ -644,7 +743,7 @@
    *
    * All addresses in @a r are set to have the @a payload.
    */
-  self_type & mark(IP4Range const &r, PAYLOAD const &payload);
+  self_type & mark(IPRange const &range, PAYLOAD const &payload);
 
   /** Fill the @a range with @a payload.
    *
@@ -704,13 +803,256 @@
   /// Remove all ranges.
   void clear();
 
-  typename IP4Space::iterator begin() { return _ip4.begin(); }
-  typename IP4Space::iterator end() { return _ip4.end(); }
+  /** Constant iterator.
+   * THe value type is a tuple of the IP address range and the @a PAYLOAD. Both are constant.
+   *
+   * @internal THe non-const iterator is a subclass of this, in order to share implementation. This
+   * also makes it easy to convert from iterator to const iterator, which is desirable.
+   */
+  class const_iterator {
+    using self_type = const_iterator; ///< Self reference type.
+    friend class IPSpace;
+  public:
+    using value_type = std::tuple<IPRange const, PAYLOAD const&>; /// Import for API compliance.
+    // STL algorithm compliance.
+    using iterator_category = std::bidirectional_iterator_tag;
+    using pointer           = value_type *;
+    using reference         = value_type &;
+    using difference_type   = int;
+
+    /// Default constructor.
+    const_iterator() = default;
+
+    /// Pre-increment.
+    /// Move to the next element in the list.
+    /// @return The iterator.
+    self_type &operator++();
+
+    /// Pre-decrement.
+    /// Move to the previous element in the list.
+    /// @return The iterator.
+    self_type &operator--();
+
+    /// Post-increment.
+    /// Move to the next element in the list.
+    /// @return The iterator value before the increment.
+    self_type operator++(int);
+
+    /// Post-decrement.
+    /// Move to the previous element in the list.
+    /// @return The iterator value before the decrement.
+    self_type operator--(int);
+
+    /// Dereference.
+    /// @return A reference to the referent.
+    value_type const& operator*() const;
+
+    /// Dereference.
+    /// @return A pointer to the referent.
+    value_type const* operator->() const;
+
+    /// Equality
+    bool operator==(self_type const &that) const;
+
+    /// Inequality
+    bool operator!=(self_type const &that) const;
+
+  protected:
+    // These are stored non-const to make implementing @c iterator easier. This class provides the
+    // required @c const protection. This is basic a tuple of iterators - for forward iteration if
+    // the primary (ipv4) iterator is at the end, then use the secondary (ipv6) iterator. The reverse
+    // is done for reverse iteration. This depends on the extra support @c IntrusiveDList iterators
+    // provide.
+    typename IP4Space::iterator _iter_4; ///< IPv4 sub-space iterator.
+    typename IP6Space::iterator _iter_6; ///< IPv6 sub-space iterator.
+    /// Current value.
+    value_type _value { IPRange{}, *null_payload };
+
+    /// Dummy payload.
+    /// @internal Used to initialize @c value_type for invalid iterators.
+    static constexpr PAYLOAD *  null_payload = nullptr;
+
+    /** Internal constructor.
+     *
+     * @param iter4 Starting place for IPv4 subspace.
+     * @param iter6 Starting place for IPv6 subspace.
+     *
+     * In practice, both iterators should be either the beginning or ending iterator for the subspace.
+     */
+    const_iterator(typename IP4Space::iterator const& iter4, typename IP6Space::iterator const& iter6);
+  };
+
+  /** Iterator.
+   * THe value type is a tuple of the IP address range and the @a PAYLOAD. The range is constant
+   * and the @a PAYLOAD is a reference. This can be used to update the @a PAYLOAD for this range.
+   *
+   * @note Range merges are not trigged by modifications of the @a PAYLOAD via an iterator.
+   */
+  class iterator : public const_iterator {
+    using self_type = iterator;
+    using super_type = const_iterator;
+    friend class IPSpace;
+  public:
+  public:
+    /// Value type of iteration.
+    using value_type = std::tuple<IPRange const, PAYLOAD&>;
+    using pointer           = value_type *;
+    using reference         = value_type &;
+
+    /// Default constructor.
+    iterator() = default;
+
+    /// Pre-increment.
+    /// Move to the next element in the list.
+    /// @return The iterator.
+    self_type &operator++();
+
+    /// Pre-decrement.
+    /// Move to the previous element in the list.
+    /// @return The iterator.
+    self_type &operator--();
+
+    /// Post-increment.
+    /// Move to the next element in the list.
+    /// @return The iterator value before the increment.
+    self_type operator++(int) { self_type zret{*this}; ++*this; return zret; }
+
+    /// Post-decrement.
+    /// Move to the previous element in the list.
+    /// @return The iterator value before the decrement.
+    self_type operator--(int) { self_type zret{*this}; --*this; return zret; }
+
+    /// Dereference.
+    /// @return A reference to the referent.
+    value_type const& operator*() const;
+
+    /// Dereference.
+    /// @return A pointer to the referent.
+    value_type const* operator->() const;
+
+  protected:
+    using super_type::super_type;
+  };
+
+  const_iterator begin() const { return const_iterator(_ip4.begin(), _ip6.begin()); }
+  const_iterator end() const { return const_iterator(_ip4.end(), _ip6.end()); }
+
+  iterator begin() { return iterator{_ip4.begin(), _ip6.begin()}; }
+  iterator end() { return iterator{_ip4.end(), _ip6.end()}; }
 
 protected:
-  IP4Space _ip4;
-  IP6Space _ip6;
+  IP4Space _ip4; ///< Sub-space containing IPv4 ranges.
+  IP6Space _ip6; ///< sub-space containing IPv6 ranges.
 };
+
+template<typename PAYLOAD>
+IPSpace<PAYLOAD>::const_iterator::const_iterator(typename IP4Space::iterator const& iter4, typename IP6Space::iterator const& iter6) : _iter_4(iter4), _iter_6(iter6) {
+  if (_iter_4.has_next()) {
+    new(&_value) value_type{_iter_4->range(), _iter_4->payload()};
+  } else if (_iter_6.has_next()) {
+    new(&_value) value_type{_iter_6->range(), _iter_6->payload()};
+  }
+}
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator++() -> self_type & {
+  bool incr_p = false;
+  if (_iter_4.has_next()) {
+    ++_iter_4;
+    incr_p = true;
+    if (_iter_4.has_next()) {
+      new(&_value) value_type{_iter_4->range(), _iter_4->payload()};
+      return *this;
+    }
+  }
+
+  if (_iter_6.has_next()) {
+    if (incr_p || (++_iter_6).has_next()) {
+      new(&_value) value_type{_iter_6->range(), _iter_6->payload()};
+      return *this;
+    }
+  }
+  new (&_value) value_type{IPRange{}, *null_payload};
+  return *this;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator++(int) -> self_type {
+  self_type zret(*this);
+  ++*this;
+  return zret;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator--() -> self_type & {
+  if (_iter_6.has_prev()) {
+    --_iter_6;
+    new (&_value) value_type{_iter_6->range(), _iter_6->payload()};
+    return *this;
+  }
+  if (_iter_4.has_prev()) {
+    --_iter_4;
+    new(&_value) value_type{_iter_4->range(), _iter_4->payload()};
+    return *this;
+  }
+  new (&_value) value_type{IPRange{}, *null_payload};
+  return *this;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator--(int) -> self_type {
+  self_type zret(*this);
+  --*this;
+  return zret;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator*() const -> value_type const& { return _value; }
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::const_iterator::operator->() const -> value_type const * { return &_value; }
+
+/* Bit of subtlety with equality - although it seems that if @a _iter_4 is valid, it doesn't matter
+ * where @a _iter6 is (because it is really the iterator location that's being checked), it's
+ * neccesary to do the @a _iter_4 validity on both iterators to avoid the case of a false positive
+ * where different internal iterators are valid. However, in practice the other (non-active)
+ * iterator won't have an arbitrary value, it will be either @c begin or @c end in step with the
+ * active iterator therefore it's effective and cheaper to just check both values.
+ */
+
+template<typename PAYLOAD>
+bool
+IPSpace<PAYLOAD>::const_iterator::operator==(self_type const& that) const {
+  return _iter_4 == that._iter_4 && _iter_6 == that._iter_6;
+}
+
+template<typename PAYLOAD>
+bool
+IPSpace<PAYLOAD>::const_iterator::operator!=(self_type const& that) const {
+  return _iter_4 != that._iter_4 || _iter_6 != that._iter_6;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::iterator::operator->() const -> value_type const* {
+  return static_cast<value_type*>(&super_type::_value);
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::iterator::operator*() const -> value_type const& {
+  return reinterpret_cast<value_type const&>(super_type::_value);
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::iterator::operator++() -> self_type & {
+  this->super_type::operator++();
+  return *this;
+}
+
+template<typename PAYLOAD>
+auto IPSpace<PAYLOAD>::iterator::operator--() -> self_type & {
+  this->super_type::operator--();
+  return *this;
+}
+
 // --------------------------------------------------------------------------
 
 // @c constexpr constructor is required to initialize _something_, it can't be completely uninitializing.
@@ -728,6 +1070,10 @@
   this->assign(&addr.sa);
 }
 
+inline IPAddr::IPAddr(string_view const& text) {
+  this->load(text);
+}
+
 inline IPAddr &
 IPAddr::operator=(in_addr_t addr) {
   _family    = AF_INET;
@@ -1001,13 +1347,13 @@
 }
 
 inline in_port_t
-IPEndpoint::host_order_port(sockaddr const *addr) {
-  return ntohs(self_type::port(addr));
+IPEndpoint::host_order_port(sockaddr const *sa) {
+  return ntohs(self_type::port(sa));
 }
 
 // --- IPAddr variants ---
 
-inline constexpr IP4Addr::IP4Addr(in_addr_t addr) : _addr(reorder(addr)) {}
+inline constexpr IP4Addr::IP4Addr(in_addr_t addr) : _addr(addr) {}
 
 inline IP4Addr::IP4Addr(std::string_view const& text) {
   if (! this->load(text)) {
@@ -1015,6 +1361,8 @@
   }
 }
 
+inline IP4Addr::IP4Addr(IPAddr const& addr) : _addr(addr._family == AF_INET ? addr._addr._ip4._addr : INADDR_ANY) {}
+
 inline IP4Addr &
 IP4Addr::operator++() {
   ++_addr;
@@ -1035,10 +1383,6 @@
   return _addr;
 }
 
-inline IP4Addr::operator in_addr_t() const {
-  return this->network_order();
-}
-
 inline auto
 IP4Addr::operator=(in_addr_t ip) -> self_type & {
   _addr = ntohl(ip);
@@ -1083,6 +1427,10 @@
   return *this;
 }
 
+constexpr in_addr_t IP4Addr::reorder(in_addr_t src) {
+  return ((src & 0xFF) << 24) | (((src >> 8) & 0xFF) << 16) | (((src >> 16) & 0xFF) << 8) | ((src >> 24) & 0xFF);
+}
+
 // ---
 
 inline IP6Addr::IP6Addr(in6_addr const& addr) {
@@ -1095,6 +1443,8 @@
   }
 }
 
+inline IP6Addr::IP6Addr(IPAddr const& addr) : _addr{addr._addr._ip6._addr} {}
+
 inline in6_addr& IP6Addr::copy_to(in6_addr & addr) const {
   self_type::reorder(addr, _addr._raw);
   return addr;
@@ -1165,8 +1515,60 @@
   return rhs <= lhs;
 }
 
+// Disambiguating comparisons.
+
+inline bool operator == (IPAddr const& lhs, IP4Addr const& rhs) {
+  return lhs.is_ip4() && static_cast<IP4Addr const&>(lhs) == rhs;
+}
+
+inline bool operator != (IPAddr const& lhs, IP4Addr const& rhs) {
+  return ! lhs.is_ip4() || static_cast<IP4Addr const&>(lhs) != rhs;
+}
+
+inline bool operator == (IP4Addr const& lhs, IPAddr const& rhs) {
+  return rhs.is_ip4() && lhs == static_cast<IP4Addr const&>(rhs);
+}
+
+inline bool operator != (IP4Addr const& lhs, IPAddr const& rhs) {
+  return ! rhs.is_ip4() || lhs != static_cast<IP4Addr const&>(rhs);
+}
+
+inline bool operator == (IPAddr const& lhs, IP6Addr const& rhs) {
+  return lhs.is_ip6() && static_cast<IP6Addr const&>(lhs) == rhs;
+}
+
+inline bool operator != (IPAddr const& lhs, IP6Addr const& rhs) {
+  return ! lhs.is_ip6() || static_cast<IP6Addr const&>(lhs) != rhs;
+}
+
+inline bool operator == (IP6Addr const& lhs, IPAddr const& rhs) {
+  return rhs.is_ip6() && lhs == static_cast<IP6Addr const&>(rhs);
+}
+
+inline bool operator != (IP6Addr const& lhs, IPAddr const& rhs) {
+  return ! rhs.is_ip6() || lhs != static_cast<IP6Addr const&>(rhs);
+}
+
 // +++ IPRange +++
 
+inline IP4Range::IP4Range(string_view const& text) {
+  this->load(text);
+}
+
+inline IPRange::IPRange(IP4Range const& range) : _family(AF_INET) {
+  _range._ip4 = range;
+}
+
+inline IPRange::IPRange(IP6Range const& range) : _family(AF_INET6) {
+  _range._ip6 = range;
+}
+
+inline IPRange::IPRange(string_view const& text) {
+  this->load(text);
+}
+
+inline bool IPRange::is(sa_family_t family) const { return family == _family; }
+
 // +++ IpMask +++
 
 inline IPMask::IPMask(raw_type width, sa_family_t family) : _mask(width), _family(family) {}
@@ -1215,12 +1617,16 @@
 
 // --- IPSpace
 
-template < typename PAYLOAD > auto IPSpace<PAYLOAD>::mark(swoc::IP4Range const &r, PAYLOAD const &payload) -> self_type & {
-  _ip4.mark(r, payload);
+template < typename PAYLOAD > auto IPSpace<PAYLOAD>::mark(IPRange const &range, PAYLOAD const &payload) -> self_type & {
+  if (range.is(AF_INET)) {
+    _ip4.mark(range, payload);
+  } else if (range.is(AF_INET6)) {
+    _ip6.mark(range, payload);
+  }
   return *this;
 }
 
-template < typename PAYLOAD > auto IPSpace<PAYLOAD>::fill(swoc::IPRange const &range, PAYLOAD const &payload) -> self_type & {
+template < typename PAYLOAD > auto IPSpace<PAYLOAD>::fill(IPRange const &range, PAYLOAD const &payload) -> self_type & {
   if (range.is(AF_INET6)) {
     _ip6.fill(range, payload);
   } else if (range.is(AF_INET)) {
diff --git a/swoc++/src/bw_ip_format.cc b/swoc++/src/bw_ip_format.cc
index 14f5c27..a87969c 100644
--- a/swoc++/src/bw_ip_format.cc
+++ b/swoc++/src/bw_ip_format.cc
@@ -49,40 +49,6 @@
 using bwf::Spec;
 
 BufferWriter &
-bwformat(BufferWriter &w, Spec const &spec, IP4Addr const& addr)
-{
-  in_addr_t host = addr.host_order();
-  Spec local_spec{spec}; // Format for address elements.
-  bool align_p = false;
-
-  if (spec._ext.size()) {
-    if (spec._ext.front() == '=') {
-      align_p          = true;
-      local_spec._fill = '0';
-    } else if (spec._ext.size() > 1 && spec._ext[1] == '=') {
-      align_p          = true;
-      local_spec._fill = spec._ext[0];
-    }
-  }
-
-  if (align_p) {
-    local_spec._min   = 3;
-    local_spec._align = Spec::Align::RIGHT;
-  } else {
-    local_spec._min = 0;
-  }
-
-  bwformat(w, local_spec, static_cast<uint8_t>(host >> 24 & 0xFF));
-  w.write('.');
-  bwformat(w, local_spec, static_cast<uint8_t>(host >> 16 & 0xFF));
-  w.write('.');
-  bwformat(w, local_spec, static_cast<uint8_t>(host >> 8 & 0xFF));
-  w.write('.');
-  bwformat(w, local_spec, static_cast<uint8_t>(host & 0xFF));
-  return w;
-}
-
-BufferWriter &
 bwformat(BufferWriter &w, Spec const &spec, in6_addr const &addr)
 {
   using QUAD = uint16_t const;
@@ -151,6 +117,136 @@
 }
 
 BufferWriter &
+bwformat(BufferWriter &w, Spec const &spec, sockaddr const *addr)
+{
+  Spec local_spec{spec}; // Format for address elements and port.
+  bool port_p{true};
+  bool addr_p{true};
+  bool family_p{false};
+  bool local_numeric_fill_p{false};
+  char local_numeric_fill_char{'0'};
+
+  if (spec._type == 'p' || spec._type == 'P') {
+    bwformat(w, spec, static_cast<void const *>(addr));
+    return w;
+  }
+
+  if (spec._ext.size()) {
+    if (spec._ext.front() == '=') {
+      local_numeric_fill_p = true;
+      local_spec._ext.remove_prefix(1);
+    } else if (spec._ext.size() > 1 && spec._ext[1] == '=') {
+      local_numeric_fill_p    = true;
+      local_numeric_fill_char = spec._ext.front();
+      local_spec._ext.remove_prefix(2);
+    }
+  }
+  if (local_spec._ext.size()) {
+    addr_p = port_p = false;
+    for (char c : local_spec._ext) {
+      switch (c) {
+        case 'a':
+        case 'A':
+          addr_p = true;
+          break;
+        case 'p':
+        case 'P':
+          port_p = true;
+          break;
+        case 'f':
+        case 'F':
+          family_p = true;
+          break;
+      }
+    }
+  }
+
+  if (addr_p) {
+    bool bracket_p = false;
+    switch (addr->sa_family) {
+      case AF_INET:
+        bwformat(w, spec, IP4Addr{IP4Addr::reorder(reinterpret_cast<sockaddr_in const *>(addr)->sin_addr.s_addr)});
+        break;
+      case AF_INET6:
+        if (port_p) {
+          w.write('[');
+          bracket_p = true; // take a note - put in the trailing bracket.
+        }
+        bwformat(w, spec, reinterpret_cast<sockaddr_in6 const *>(addr)->sin6_addr);
+        break;
+      default:
+        w.print("*Not IP address [{}]*", addr->sa_family);
+        break;
+    }
+    if (bracket_p)
+      w.write(']');
+    if (port_p)
+      w.write(':');
+  }
+  if (port_p) {
+    if (local_numeric_fill_p) {
+      local_spec._min   = 5;
+      local_spec._fill  = local_numeric_fill_char;
+      local_spec._align = Spec::Align::RIGHT;
+    } else {
+      local_spec._min = 0;
+    }
+    bwformat(w, local_spec, static_cast<uintmax_t>(IPEndpoint::host_order_port(addr)));
+  }
+  if (family_p) {
+    local_spec._min = 0;
+    if (addr_p || port_p)
+      w.write(' ');
+    if (spec.has_numeric_type()) {
+      bwformat(w, local_spec, static_cast<uintmax_t>(addr->sa_family));
+    } else {
+      swoc::bwformat(w, local_spec, IPEndpoint::family_name(addr->sa_family));
+    }
+  }
+  return w;
+}
+
+BufferWriter &
+bwformat(BufferWriter &w, Spec const &spec, IP4Addr const& addr)
+{
+  in_addr_t host = addr.host_order();
+  Spec local_spec{spec}; // Format for address elements.
+  bool align_p = false;
+
+  if (spec._ext.size()) {
+    if (spec._ext.front() == '=') {
+      align_p          = true;
+      local_spec._fill = '0';
+    } else if (spec._ext.size() > 1 && spec._ext[1] == '=') {
+      align_p          = true;
+      local_spec._fill = spec._ext[0];
+    }
+  }
+
+  if (align_p) {
+    local_spec._min   = 3;
+    local_spec._align = Spec::Align::RIGHT;
+  } else {
+    local_spec._min = 0;
+  }
+
+  bwformat(w, local_spec, static_cast<uint8_t>(host >> 24 & 0xFF));
+  w.write('.');
+  bwformat(w, local_spec, static_cast<uint8_t>(host >> 16 & 0xFF));
+  w.write('.');
+  bwformat(w, local_spec, static_cast<uint8_t>(host >> 8 & 0xFF));
+  w.write('.');
+  bwformat(w, local_spec, static_cast<uint8_t>(host & 0xFF));
+  return w;
+}
+
+BufferWriter &
+bwformat(BufferWriter &w, Spec const &spec, IP6Addr const& addr)
+{
+  return bwformat(w, spec, addr.network_order());
+}
+
+BufferWriter &
 bwformat(BufferWriter &w, Spec const &spec, IPAddr const &addr)
 {
   Spec local_spec{spec}; // Format for address elements and port.
@@ -182,7 +278,7 @@
 
   if (addr_p) {
     if (addr.is_ip4()) {
-      swoc::bwformat(w, spec, addr.network_ip4());
+      swoc::bwformat(w, spec, static_cast<IP4Addr const&>(addr));
     } else if (addr.is_ip6()) {
       swoc::bwformat(w, spec, addr.network_ip6());
     } else {
@@ -205,93 +301,28 @@
 }
 
 BufferWriter &
-bwformat(BufferWriter &w, Spec const &spec, sockaddr const *addr)
-{
-  Spec local_spec{spec}; // Format for address elements and port.
-  bool port_p{true};
-  bool addr_p{true};
-  bool family_p{false};
-  bool local_numeric_fill_p{false};
-  char local_numeric_fill_char{'0'};
-
-  if (spec._type == 'p' || spec._type == 'P') {
-    bwformat(w, spec, static_cast<void const *>(addr));
-    return w;
-  }
-
-  if (spec._ext.size()) {
-    if (spec._ext.front() == '=') {
-      local_numeric_fill_p = true;
-      local_spec._ext.remove_prefix(1);
-    } else if (spec._ext.size() > 1 && spec._ext[1] == '=') {
-      local_numeric_fill_p    = true;
-      local_numeric_fill_char = spec._ext.front();
-      local_spec._ext.remove_prefix(2);
-    }
-  }
-  if (local_spec._ext.size()) {
-    addr_p = port_p = false;
-    for (char c : local_spec._ext) {
-      switch (c) {
-      case 'a':
-      case 'A':
-        addr_p = true;
-        break;
-      case 'p':
-      case 'P':
-        port_p = true;
-        break;
-      case 'f':
-      case 'F':
-        family_p = true;
-        break;
-      }
-    }
-  }
-
-  if (addr_p) {
-    bool bracket_p = false;
-    switch (addr->sa_family) {
-    case AF_INET:
-      bwformat(w, spec, IP4Addr{reinterpret_cast<sockaddr_in const *>(addr)->sin_addr.s_addr});
-      break;
-    case AF_INET6:
-      if (port_p) {
-        w.write('[');
-        bracket_p = true; // take a note - put in the trailing bracket.
-      }
-      bwformat(w, spec, reinterpret_cast<sockaddr_in6 const *>(addr)->sin6_addr);
-      break;
-    default:
-      w.print("*Not IP address [{}]*", addr->sa_family);
-      break;
-    }
-    if (bracket_p)
-      w.write(']');
-    if (port_p)
-      w.write(':');
-  }
-  if (port_p) {
-    if (local_numeric_fill_p) {
-      local_spec._min   = 5;
-      local_spec._fill  = local_numeric_fill_char;
-      local_spec._align = Spec::Align::RIGHT;
-    } else {
-      local_spec._min = 0;
-    }
-    bwformat(w, local_spec, static_cast<uintmax_t>(IPEndpoint::host_order_port(addr)));
-  }
-  if (family_p) {
-    local_spec._min = 0;
-    if (addr_p || port_p)
-      w.write(' ');
-    if (spec.has_numeric_type()) {
-      bwformat(w, local_spec, static_cast<uintmax_t>(addr->sa_family));
-    } else {
-      swoc::bwformat(w, local_spec, IPEndpoint::family_name(addr->sa_family));
-    }
-  }
-  return w;
+bwformat(BufferWriter & w, Spec const& spec, IP4Range const& range) {
+  return range.is_empty()
+  ? w.write("*-*"_tv)
+  : w.print("{}-{}", range.min(), range.max());
 }
 
+BufferWriter &
+bwformat(BufferWriter & w, Spec const& spec, IP6Range const& range) {
+  return range.is_empty()
+         ? w.write("*-*"_tv)
+         : w.print("{}-{}", range.min(), range.max());
+}
+
+BufferWriter &
+bwformat(BufferWriter & w, Spec const& spec, IPRange const& range) {
+  return range.is(AF_INET)
+  ? bwformat(w, spec, static_cast<IP4Range const&>(range))
+  : range.is(AF_INET6)
+    ? bwformat(w, spec, static_cast<IP6Range const&>(range))
+    : w.write("*-*"_tv)
+  ;
+}
+
+
 } // namespace swoc
diff --git a/swoc++/src/swoc_ip.cc b/swoc++/src/swoc_ip.cc
index 076db6b..1c1675e 100644
--- a/swoc++/src/swoc_ip.cc
+++ b/swoc++/src/swoc_ip.cc
@@ -27,26 +27,23 @@
 using swoc::svtou;
 using namespace swoc::literals;
 
-namespace
-{
+namespace {
 // Handle the @c sin_len member, the presence of which varies across compilation environments.
-template <typename T>
+template<typename T>
 auto
-Set_Sockaddr_Len_Case(T *, swoc::meta::CaseTag<0>) -> decltype(swoc::meta::TypeFunc<void>())
-{
+Set_Sockaddr_Len_Case(T *, swoc::meta::CaseTag<0>) -> decltype(swoc::meta::TypeFunc<void>()) {
 }
 
-template <typename T>
+template<typename T>
 auto
-Set_Sockaddr_Len_Case(T *addr, swoc::meta::CaseTag<1>) -> decltype(T::sin_len, swoc::meta::TypeFunc<void>())
-{
+Set_Sockaddr_Len_Case(T *addr
+                      , swoc::meta::CaseTag<1>) -> decltype(T::sin_len, swoc::meta::TypeFunc<void>()) {
   addr->sin_len = sizeof(T);
 }
 
-template <typename T>
+template<typename T>
 void
-Set_Sockaddr_Len(T *addr)
-{
+Set_Sockaddr_Len(T *addr) {
   Set_Sockaddr_Len_Case(addr, swoc::meta::CaseArg);
 }
 
@@ -197,15 +194,15 @@
 IPAddr::fill(sockaddr *sa, in_port_t port) const {
   switch (sa->sa_family = _family) {
     case AF_INET: {
-      sockaddr_in *sa4{reinterpret_cast<sockaddr_in *>(sa)};
+      auto sa4 = reinterpret_cast<sockaddr_in *>(sa);
       memset(sa4, 0, sizeof(*sa4));
-      sa4->sin_addr.s_addr = _addr._ip4;
+      sa4->sin_addr.s_addr = _addr._ip4.network_order();
       sa4->sin_port = port;
       Set_Sockaddr_Len(sa4);
     }
       break;
     case AF_INET6: {
-      sockaddr_in6 *sa6{reinterpret_cast<sockaddr_in6 *>(sa)};
+      auto sa6 = reinterpret_cast<sockaddr_in6 *>(sa);
       memset(sa6, 0, sizeof(*sa6));
       sa6->sin6_port = port;
       Set_Sockaddr_Len(sa6);
@@ -293,6 +290,18 @@
   return false;
 }
 
+IP4Addr::IP4Addr(sockaddr_in const *sa) : _addr(reorder(sa->sin_addr.s_addr)) {}
+
+auto IP4Addr::operator=(sockaddr_in const *sa) -> self_type& {
+  _addr = reorder(sa->sin_addr.s_addr);
+  return *this;
+}
+
+sockaddr_in *IP4Addr::fill(sockaddr_in *sa, in_port_t port) const {
+  sa->sin_addr.s_addr = this->network_order();
+  return sa;
+}
+
 bool
 IP6Addr::load(std::string_view const&str) {
   TextView src{str};
@@ -559,22 +568,22 @@
   return false;
 }
 
-IP6Range & IP6Range::assign(IP6Addr const&addr, IPMask const&mask) {
-  static constexpr auto FULL_MASK { std::numeric_limits<uint64_t>::max() };
+IP6Range&IP6Range::assign(IP6Addr const&addr, IPMask const&mask) {
+  static constexpr auto FULL_MASK{std::numeric_limits<uint64_t>::max()};
   auto cidr = mask.width();
   if (cidr == 0) {
     _min = metric_type::MIN;
     _max = metric_type::MAX;
   } else if (cidr < 64) { // only upper bytes affected, lower bytes are forced.
-    auto bits          = FULL_MASK << (64 - cidr);
-    _min._addr._u64[0]           = addr._addr._u64[0] & bits;
-    _min._addr._u64[1]           = 0;
-    _max._addr._u64[0]           = addr._addr._u64[0] | ~bits;
-    _max._addr._u64[1]           = FULL_MASK;
+    auto bits = FULL_MASK << (64 - cidr);
+    _min._addr._u64[0] = addr._addr._u64[0] & bits;
+    _min._addr._u64[1] = 0;
+    _max._addr._u64[0] = addr._addr._u64[0] | ~bits;
+    _max._addr._u64[1] = FULL_MASK;
   } else if (cidr == 64) {
     _min._addr._u64[0] = _max._addr._u64[0] = addr._addr._u64[0];
-    _min._addr._u64[1]                       = 0;
-    _max._addr._u64[1]                       = FULL_MASK;
+    _min._addr._u64[1] = 0;
+    _max._addr._u64[1] = FULL_MASK;
   } else if (cidr <= 128) { // _min bytes changed, _max bytes unaffected.
     _min = _max = addr;
     if (cidr < 128) {
@@ -613,7 +622,7 @@
   return false;
 }
 
-bool IPRange::load(std::string_view const& text) {
+bool IPRange::load(std::string_view const&text) {
   static const string_view CHARS{".:"};
   auto idx = text.find_first_of(CHARS);
   if (idx != text.npos) {
@@ -633,7 +642,7 @@
 }
 
 IPAddr IPRange::min() const {
-  switch(_family) {
+  switch (_family) {
     case AF_INET: return _range._ip4.min();
     case AF_INET6: return _range._ip6.min();
     default: break;
@@ -642,7 +651,7 @@
 }
 
 IPAddr IPRange::max() const {
-  switch(_family) {
+  switch (_family) {
     case AF_INET: return _range._ip4.max();
     case AF_INET6: return _range._ip6.max();
     default: break;
diff --git a/unit_tests/test_ip.cc b/unit_tests/test_ip.cc
index 285e13f..b210666 100644
--- a/unit_tests/test_ip.cc
+++ b/unit_tests/test_ip.cc
@@ -19,11 +19,16 @@
 using namespace swoc::literals;
 using swoc::TextView;
 using swoc::IPEndpoint;
+
 using swoc::IP4Addr;
 using swoc::IP4Range;
+
 using swoc::IP6Addr;
 using swoc::IP6Range;
 
+using swoc::IPAddr;
+using swoc::IPRange;
+
 TEST_CASE("ink_inet", "[libswoc][ip]") {
   // Use TextView because string_view(nullptr) fails. Gah.
   struct ip_parse_spec {
@@ -82,6 +87,10 @@
   IP4Addr a4_1{"172.28.56.33"};
   IP4Addr a4_2{"172.28.56.34"};
   IP4Addr a4_3{"170.28.56.35"};
+  IP4Addr a4_loopback{"127.0.0.1"_tv};
+  IP4Addr ip4_loopback{INADDR_LOOPBACK};
+
+  REQUIRE(a4_loopback == ip4_loopback);
 
   REQUIRE(a4_1 != a4_null);
   REQUIRE(a4_1 != a4_2);
@@ -150,8 +159,16 @@
   std::string_view localhost{"[::1]:8080"};
   swoc::LocalBufferWriter<1024> w;
 
+  REQUIRE(ep.parse(addr_null) == true);
+  w.clear().print("{::a}", ep);
+  REQUIRE(w.view() == "::");
+
+  ep.set_to_loopback(AF_INET6);
+  w.clear().print("{::a}", ep);
+  REQUIRE(w.view() == "::1");
+
   REQUIRE(ep.parse(addr_1) == true);
-  w.print("{}", ep);
+  w.clear().print("{}", ep);
   REQUIRE(w.view() == addr_1);
   w.clear().print("{::p}", ep);
   REQUIRE(w.view() == "8080");
@@ -166,30 +183,6 @@
   w.clear().print("{:: =a}", ep);
   REQUIRE(w.view() == "ffee:   0:   0:   0:24c3:3349:3cee: 143");
 
-#if 0
-  ep.setToLoopback(AF_INET6);
-  w.reset().print("{::a}", ep);
-  REQUIRE(w.view() == "::1");
-  REQUIRE(0 == ats_ip_pton(addr_3, &ep.sa));
-  w.reset().print("{::a}", ep);
-  REQUIRE(w.view() == "1337:ded:beef::");
-  REQUIRE(0 == ats_ip_pton(addr_4, &ep.sa));
-  w.reset().print("{::a}", ep);
-  REQUIRE(w.view() == "1337::ded:beef");
-
-  REQUIRE(0 == ats_ip_pton(addr_5, &ep.sa));
-  w.reset().print("{:X:a}", ep);
-  REQUIRE(w.view() == "1337::DED:BEEF:0:0:956");
-
-  REQUIRE(0 == ats_ip_pton(addr_6, &ep.sa));
-  w.reset().print("{::a}", ep);
-  REQUIRE(w.view() == "1337:0:0:ded:beef::");
-
-  REQUIRE(0 == ats_ip_pton(addr_null, &ep.sa));
-  w.reset().print("{::a}", ep);
-  REQUIRE(w.view() == "::");
-#endif
-
   REQUIRE(ep.parse(addr_2) == true);
   w.clear().print("{::a}", ep);
   REQUIRE(w.view() == addr_2.substr(0, 13));
@@ -212,40 +205,47 @@
   w.clear().print("{::=a}", ep);
   REQUIRE(w.view() == "172.017.099.231");
 
-#if 0
+  REQUIRE(ep.parse(addr_3) == true);
+  w.clear().print("{::a}", ep);
+  REQUIRE(w.view() == "1337:ded:beef::"_tv);
+
+  REQUIRE(ep.parse(addr_4) == true);
+  w.clear().print("{::a}", ep);
+  REQUIRE(w.view() == "1337::ded:beef"_tv);
+
+  REQUIRE(ep.parse(addr_5) == true);
+  w.clear().print("{:X:a}", ep);
+  REQUIRE(w.view() == "1337::DED:BEEF:0:0:956");
+
+  REQUIRE(ep.parse(addr_6) == true);
+  w.clear().print("{::a}", ep);
+  REQUIRE(w.view() == "1337:0:0:ded:beef::");
+
   // Documentation examples
-  REQUIRE(0 == ats_ip_pton(addr_7, &ep.sa));
-  w.reset().print("To {}", ep);
+  REQUIRE(ep.parse(addr_7) == true);
+  w.clear().print("To {}", ep);
   REQUIRE(w.view() == "To 172.19.3.105:4951");
-  w.reset().print("To {0::a} on port {0::p}", ep); // no need to pass the argument twice.
+  w.clear().print("To {0::a} on port {0::p}", ep); // no need to pass the argument twice.
   REQUIRE(w.view() == "To 172.19.3.105 on port 4951");
-  w.reset().print("To {::=}", ep);
+  w.clear().print("To {::=}", ep);
   REQUIRE(w.view() == "To 172.019.003.105:04951");
-  w.reset().print("{::a}", ep);
+  w.clear().print("{::a}", ep);
   REQUIRE(w.view() == "172.19.3.105");
-  w.reset().print("{::=a}", ep);
+  w.clear().print("{::=a}", ep);
   REQUIRE(w.view() == "172.019.003.105");
-  w.reset().print("{::0=a}", ep);
+  w.clear().print("{::0=a}", ep);
   REQUIRE(w.view() == "172.019.003.105");
-  w.reset().print("{:: =a}", ep);
+  w.clear().print("{:: =a}", ep);
   REQUIRE(w.view() == "172. 19.  3.105");
-  w.reset().print("{:>20:a}", ep);
+  w.clear().print("{:>20:a}", ep);
   REQUIRE(w.view() == "        172.19.3.105");
-  w.reset().print("{:>20:=a}", ep);
+  w.clear().print("{:>20:=a}", ep);
   REQUIRE(w.view() == "     172.019.003.105");
-  w.reset().print("{:>20: =a}", ep);
+  w.clear().print("{:>20: =a}", ep);
   REQUIRE(w.view() == "     172. 19.  3.105");
-  w.reset().print("{:<20:a}", ep);
+  w.clear().print("{:<20:a}", ep);
   REQUIRE(w.view() == "172.19.3.105        ");
 
-  w.reset().print("{:p}", reinterpret_cast<sockaddr const *>(0x1337beef));
-  REQUIRE(w.view() == "0x1337beef");
-
-  ats_ip_pton(addr_1, &ep.sa);
-  w.reset().print("{}", swoc::bwf::Hex_Dump(ep));
-  REQUIRE(w.view() == "ffee00000000000024c333493cee0143");
-#endif
-
   REQUIRE(ep.parse(localhost) == true);
   w.clear().print("{}", ep);
   REQUIRE(w.view() == localhost);
@@ -317,17 +317,17 @@
 TEST_CASE("IP Space Int", "[libswoc][ip][ipspace]") {
   using int_space = swoc::IPSpace<unsigned>;
   int_space space;
-  auto dump = [] (int_space & space) -> void {
+  auto dump = [](int_space&space) -> void {
     swoc::LocalBufferWriter<1024> w;
     std::cout << "Dumping " << space.count() << " ranges" << std::endl;
-    for ( auto & r : space ) {
-      std::cout << w.clear().print("{} - {} : {}\n", r.min(), r.max(), r.payload()).view();
+    for (auto &&[r, payload] : space) {
+      std::cout << w.clear().print("{} - {} : {}\n", r.min(), r.max(), payload).view();
     }
   };
 
   REQUIRE(space.count() == 0);
 
-  space.mark({IP4Addr("172.16.0.0"), IP4Addr("172.16.0.255")}, 1);
+  space.mark(IPRange{{IP4Addr("172.16.0.0"), IP4Addr("172.16.0.255")}}, 1);
   auto result = space.find({"172.16.0.97"});
   REQUIRE(result != nullptr);
   REQUIRE(*result == 1);
@@ -335,7 +335,7 @@
   result = space.find({"172.17.0.97"});
   REQUIRE(result == nullptr);
 
-  space.mark({IP4Addr("172.16.0.12"), IP4Addr("172.16.0.25")}, 2);
+  space.mark(IPRange{"172.16.0.12-172.16.0.25"_tv}, 2);
 
   result = space.find({"172.16.0.21"});
   REQUIRE(result != nullptr);
@@ -343,12 +343,19 @@
   REQUIRE(space.count() == 3);
 
   space.clear();
-  auto BF = [](unsigned&lhs, unsigned rhs) -> bool { lhs |= rhs; return true; };
+  auto BF = [](unsigned&lhs, unsigned rhs) -> bool {
+    lhs |= rhs;
+    return true;
+  };
   unsigned *payload;
   swoc::IP4Range r_1{"1.1.1.0-1.1.1.9"};
   swoc::IP4Range r_2{"1.1.2.0-1.1.2.97"};
   swoc::IP4Range r_3{"1.1.0.0-1.2.0.0"};
 
+  // Compiler check - make sure both of these work.
+  REQUIRE(r_1.min() == IP4Addr("1.1.1.0"_tv));
+  REQUIRE(r_1.max() == IPAddr("1.1.1.9"_tv));
+
   space.blend(r_1, 0x1, BF);
   REQUIRE(space.count() == 1);
   REQUIRE(nullptr == space.find(r_2.min()));
@@ -366,7 +373,6 @@
   REQUIRE(*payload == 0x2);
 
   space.blend(r_3, 0x4, BF);
-  dump(space);
   REQUIRE(space.count() == 5);
   payload = space.find(r_2.min());
   REQUIRE(payload != nullptr);
@@ -381,11 +387,109 @@
   REQUIRE(*payload == 0x5);
 
   space.blend({r_2.min(), r_3.max()}, 0x6, BF);
-  dump(space);
   REQUIRE(space.count() == 4);
 }
 
-#if 1
+TEST_CASE("IPSpace bitset", "[libswoc][ipspace][bitset]") {
+  using PAYLOAD = std::bitset<32>;
+  using Space = swoc::IPSpace<PAYLOAD>;
+
+  auto dump = [](Space&space) -> void {
+    swoc::LocalBufferWriter<1024> w;
+    std::cout << "Dumping " << space.count() << " ranges" << std::endl;
+    for (auto &&[r, payload] : space) {
+      w.clear().print("{}-{} :", r.min(), r.max());
+      std::cout << w << payload << std::endl;
+    }
+  };
+  auto reverse_dump = [](Space&space) -> void {
+    swoc::LocalBufferWriter<1024> w;
+    std::cout << "Dumping " << space.count() << " ranges" << std::endl;
+    for (auto spot = space.end(); spot != space.begin();) {
+      auto &&[r, payload]{*--spot};
+      w.clear().print("{} :", r);
+      std::cout << w << payload << std::endl;
+    }
+  };
+
+  std::array<std::tuple<TextView, std::initializer_list<unsigned>>, 6> ranges = {
+      {
+          {"172.28.56.12-172.28.56.99"_tv, {0, 2, 3}}
+          , {"10.10.35.0/24"_tv, {1, 2}}
+          , {"192.168.56.0/25"_tv, {10, 12, 31}}
+          , {"1337::ded:beef-1337::ded:ceef"_tv, {4, 5, 6, 7}}
+          , {"ffee:1f2d:c587:24c3:9128:3349:3cee:143-ffee:1f2d:c587:24c3:9128:3349:3cFF:FFFF"_tv, {9, 10, 18}}
+          , {"10.12.148.0/23"_tv, {1, 2, 17}}
+      }};
+
+  Space space;
+
+  for (auto &&[text, bit_list] : ranges) {
+    PAYLOAD bits;
+    for (auto bit : bit_list) {
+      bits[bit] = true;
+    }
+    space.mark(IPRange{text}, bits);
+  }
+  REQUIRE(space.count() == ranges.size());
+  dump(space);
+  reverse_dump(space);
+}
+
+TEST_CASE("IPSpace docJJ", "[libswoc][ipspace][docJJ]") {
+  using PAYLOAD = std::bitset<32>;
+  using Space = swoc::IPSpace<PAYLOAD>;
+
+  auto dump = [](Space&space) -> void {
+    swoc::LocalBufferWriter<1024> w;
+    std::cout << "Dumping " << space.count() << " ranges" << std::endl;
+    for (auto &&[r, payload] : space) {
+      w.clear().print("{}-{} :", r.min(), r.max());
+      std::cout << w << payload << std::endl;
+    }
+  };
+  auto reverse_dump = [](Space&space) -> void {
+    swoc::LocalBufferWriter<1024> w;
+    std::cout << "Dumping " << space.count() << " ranges" << std::endl;
+    for (auto spot = space.end(); spot != space.begin();) {
+      auto &&[r, payload]{*--spot};
+      w.clear().print("{} :", r);
+      std::cout << w << payload << std::endl;
+    }
+  };
+
+  auto blender = [](PAYLOAD& lhs, PAYLOAD const& rhs) -> bool {
+    lhs |= rhs;
+    return true;
+  };
+
+  std::array<std::tuple<TextView, std::initializer_list<unsigned>>, 9> ranges = {
+      {
+            { "100.0.0.0-100.0.0.255", { 0 } }
+          , { "100.0.1.0-100.0.1.255", { 1 } }
+          , { "100.0.2.0-100.0.2.255", { 2 } }
+          , { "100.0.3.0-100.0.3.255", { 3 } }
+          , { "100.0.4.0-100.0.4.255", { 4 } }
+          , { "100.0.5.0-100.0.5.255", { 5 } }
+          , { "100.0.6.0-100.0.6.255", { 6 } }
+          , { "100.0.0.0-100.0.0.255", { 31 } }
+          , { "100.0.1.0-100.0.1.255", { 30 } }
+      }};
+
+  Space space;
+
+  for (auto &&[text, bit_list] : ranges) {
+    PAYLOAD bits;
+    for (auto bit : bit_list) {
+      bits[bit] = true;
+    }
+    space.blend(IPRange{text}, bits, blender);
+  }
+  dump(space);
+  reverse_dump(space);
+}
+
+#if 0
 TEST_CASE("IP Space YNETDB", "[libswoc][ipspace][ynetdb]") {
   std::set<std::string_view> Locations;
   std::set<std::string_view> Owners;