diff --git a/include/skyr/v1/containers/static_vector.hpp b/include/skyr/v1/containers/static_vector.hpp
index 66577f4a..87c55214 100644
--- a/include/skyr/v1/containers/static_vector.hpp
+++ b/include/skyr/v1/containers/static_vector.hpp
@@ -149,18 +149,30 @@ class static_vector {
 
   ///
   /// \return
-  [[nodiscard]] constexpr auto begin() const noexcept -> const_iterator {
+  [[nodiscard]] constexpr auto cbegin() const noexcept -> const_iterator {
     return impl_.begin();
   }
 
   ///
   /// \return
-  [[nodiscard]] constexpr auto end() const noexcept -> const_iterator {
+  [[nodiscard]] constexpr auto cend() const noexcept -> const_iterator {
     auto last = impl_.begin();
     std::advance(last, size_);
     return last;
   }
 
+  ///
+  /// \return
+  [[nodiscard]] constexpr auto begin() const noexcept -> const_iterator {
+    return cbegin();
+  }
+
+  ///
+  /// \return
+  [[nodiscard]] constexpr auto end() const noexcept -> const_iterator {
+    return cend();
+  }
+
 };
 }  // namespace v1
 }  // namespace skyr
diff --git a/src/v1/domain/idna.hpp b/include/skyr/v1/domain/idna.hpp
similarity index 51%
rename from src/v1/domain/idna.hpp
rename to include/skyr/v1/domain/idna.hpp
index d7ea9650..601986f7 100644
--- a/src/v1/domain/idna.hpp
+++ b/include/skyr/v1/domain/idna.hpp
@@ -6,6 +6,9 @@
 #ifndef SKYR_V1_DOMAIN_IDNA_HPP
 #define SKYR_V1_DOMAIN_IDNA_HPP
 
+#include <tl/expected.hpp>
+#include <skyr/v1/domain/errors.hpp>
+
 namespace skyr {
 inline namespace v1 {
 namespace idna {
@@ -45,6 +48,57 @@ auto code_point_status(char32_t code_point) -> idna_status;
 /// \return The code point or mapped value, depending on the status of the code
 /// point
 auto map_code_point(char32_t code_point) -> char32_t;
+
+///
+/// \tparam FwdIter
+/// \param first
+/// \param last
+/// \param use_std3_ascii_rules
+/// \param transitional_processing
+/// \return
+template <class FwdIter>
+inline auto map_code_points(
+    FwdIter first,
+    FwdIter last,
+    bool use_std3_ascii_rules,
+    bool transitional_processing) -> tl::expected<FwdIter, domain_errc> {
+  for (auto it = first; it != last; ++it) {
+    switch (code_point_status(*it)) {
+      case idna_status::disallowed:
+        return tl::make_unexpected(domain_errc::disallowed_code_point);
+      case idna_status::disallowed_std3_valid:
+        if (use_std3_ascii_rules) {
+          return tl::make_unexpected(domain_errc::disallowed_code_point);
+        } else {
+          *first++ = *it;
+        }
+        break;
+      case idna_status::disallowed_std3_mapped:
+        if (use_std3_ascii_rules) {
+          return tl::make_unexpected(domain_errc::disallowed_code_point);
+        } else {
+          *first++ = map_code_point(*it);
+        }
+        break;
+      case idna_status::ignored:
+        break;
+      case idna_status::mapped:
+        *first++ = idna::map_code_point(*it);
+        break;
+      case idna_status::deviation:
+        if (transitional_processing) {
+          *first++ = idna::map_code_point(*it);
+        } else {
+          *first++ = *it;
+        }
+        break;
+      case idna_status::valid:
+        *first++ = *it;
+        break;
+    }
+  }
+  return first;
+}
 }  // namespace idna
 }  // namespace v1
 }  // namespace skyr
diff --git a/src/v1/domain/punycode.hpp b/include/skyr/v1/domain/punycode.hpp
similarity index 100%
rename from src/v1/domain/punycode.hpp
rename to include/skyr/v1/domain/punycode.hpp
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 887d3128..b68b3833 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -27,10 +27,7 @@ target_sources(skyr-url
         v1/network/ipv4_address.cpp
         v1/network/ipv6_address.cpp
         v1/domain/domain.cpp
-        v1/domain/idna.hpp
         v1/domain/idna.cpp
-        v1/domain/idna_code_point_map_iterator.hpp
-        v1/domain/punycode.hpp
         v1/url/url.cpp
         v1/url/url_search_parameters.cpp
 
@@ -52,6 +49,8 @@ target_sources(skyr-url
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/string/starts_with.hpp
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/containers/static_vector.hpp
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/domain/errors.hpp
+        ${PROJECT_SOURCE_DIR}/include/skyr/v1/domain/idna.hpp
+        ${PROJECT_SOURCE_DIR}/include/skyr/v1/domain/punycode.hpp
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/domain/domain.hpp
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/platform/endianness.hpp
         ${PROJECT_SOURCE_DIR}/include/skyr/v1/network/ipv4_address.hpp
diff --git a/src/v1/domain/domain.cpp b/src/v1/domain/domain.cpp
index 7c3f35c3..0f997575 100644
--- a/src/v1/domain/domain.cpp
+++ b/src/v1/domain/domain.cpp
@@ -4,22 +4,20 @@
 // http://www.boost.org/LICENSE_1_0.txt)
 
 #include <algorithm>
+#include <range/v3/algorithm/copy.hpp>
+#include <range/v3/iterator.hpp>
 #include <range/v3/range/conversion.hpp>
 #include <range/v3/view/join.hpp>
 #include <range/v3/view/split.hpp>
 #include <range/v3/view/transform.hpp>
-#include <range/v3/algorithm/copy.hpp>
-#include <range/v3/iterator.hpp>
 #include <skyr/v1/containers/static_vector.hpp>
 #include <skyr/v1/domain/domain.hpp>
 #include <skyr/v1/domain/errors.hpp>
-#include <skyr/v1/unicode/ranges/transforms/u8_transform.hpp>
+#include <skyr/v1/domain/idna.hpp>
+#include <skyr/v1/domain/punycode.hpp>
 #include <skyr/v1/unicode/ranges/transforms/u32_transform.hpp>
+#include <skyr/v1/unicode/ranges/transforms/u8_transform.hpp>
 #include <skyr/v1/unicode/ranges/views/u8_view.hpp>
-#include "idna.hpp"
-#include "idna_code_point_map_iterator.hpp"
-#include "punycode.hpp"
-
 
 /// How many labels can be in a domain?
 /// https://www.farsightsecurity.com/blog/txt-record/rrlabel-20171013/
@@ -30,28 +28,6 @@
 namespace skyr {
 inline namespace v1 {
 namespace {
-template <class DomainName>
-auto map_code_points(
-    DomainName &&domain_name,
-    bool use_std3_ascii_rules,
-    bool transitional_processing,
-    std::u32string *result)
-    -> tl::expected<void, domain_errc> {
-  auto range = idna::views::map_code_points(domain_name, use_std3_ascii_rules, transitional_processing);
-  auto first = std::cbegin(range);
-  auto last = std::cend(range);
-  for (auto it = first; it != last; ++it) {
-    if (!*it) {
-      return tl::make_unexpected((*it).error());
-    }
-    result->push_back((*it).value());
-    if (result->size() == result->max_size()) {
-      return tl::make_unexpected(domain_errc::invalid_length);
-    }
-  }
-  return {};
-}
-
 auto validate_label(std::u32string_view label, [[maybe_unused]] bool use_std3_ascii_rules, bool check_hyphens,
                     [[maybe_unused]] bool check_bidi, [[maybe_unused]] bool check_joiners, bool transitional_processing)
     -> tl::expected<void, domain_errc> {
@@ -106,11 +82,21 @@ auto domain_to_ascii(
 
   using namespace std::string_view_literals;
 
-  auto u32domain_name = unicode::views::as_u8(domain_name) | unicode::transforms::to_u32;
-  auto mapped_domain_name = std::u32string{};
-  auto result = map_code_points(u32domain_name, use_std3_ascii_rules, transitional_processing, &mapped_domain_name);
-  if (!result) {
-    return tl::make_unexpected(result.error());
+  auto mapped_domain_name = unicode::as<std::u32string>(
+      unicode::views::as_u8(domain_name) | unicode::transforms::to_u32);
+  if (mapped_domain_name) {
+    auto result = idna::map_code_points(
+        std::begin(mapped_domain_name.value()),
+        std::end(mapped_domain_name.value()),
+        use_std3_ascii_rules,
+        transitional_processing);
+    if (result) {
+      mapped_domain_name.value().erase(result.value(), mapped_domain_name.value().cend());
+    } else {
+      return tl::make_unexpected(result.error());
+    }
+  } else {
+    return tl::make_unexpected(domain_errc::encoding_error);
   }
 
   static constexpr auto to_string_view = [] (auto &&label) {
@@ -118,7 +104,7 @@ auto domain_to_ascii(
   };
 
   auto labels = static_vector<std::u32string, SKYR_DOMAIN_MAX_NUM_LABELS>{};
-  for (auto &&label : mapped_domain_name | ranges::views::split(U'.') | ranges::views::transform(to_string_view)) {
+  for (auto &&label : mapped_domain_name.value() | ranges::views::split(U'.') | ranges::views::transform(to_string_view)) {
     if (labels.size() == labels.max_size()) {
       return tl::make_unexpected(domain_errc::too_many_labels);
     }
@@ -165,7 +151,7 @@ auto domain_to_ascii(
     }
   }
 
-  if (mapped_domain_name.back() == U'.') {
+  if (mapped_domain_name.value().back() == U'.') {
     labels.emplace_back();
   }
 
@@ -173,7 +159,7 @@ auto domain_to_ascii(
   constexpr auto max_label_length = 63;
 
   if (verify_dns_length) {
-    auto length = mapped_domain_name.size();
+    auto length = mapped_domain_name.value().size();
     if ((length < 1) || (length > max_domain_length)) {
       return tl::make_unexpected(domain_errc::invalid_length);
     }
diff --git a/src/v1/domain/idna.cpp b/src/v1/domain/idna.cpp
index 89451c37..25d91a8d 100644
--- a/src/v1/domain/idna.cpp
+++ b/src/v1/domain/idna.cpp
@@ -4,9 +4,9 @@
 // http://www.boost.org/LICENSE_1_0.txt)
 
 #include <algorithm>
-#include <iterator>
 #include <array>
-#include "idna.hpp"
+#include <iterator>
+#include <skyr/v1/domain/idna.hpp>
 
 namespace skyr {
 inline namespace v1 {
diff --git a/src/v1/domain/idna_code_point_map_iterator.hpp b/src/v1/domain/idna_code_point_map_iterator.hpp
deleted file mode 100644
index 2d79a073..00000000
--- a/src/v1/domain/idna_code_point_map_iterator.hpp
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright 2020 Glyn Matthews.
-// Distributed under the Boost Software License, Version 1.0.
-// (See accompanying file LICENSE_1_0.txt or copy at
-// http://www.boost.org/LICENSE_1_0.txt)
-
-#ifndef SKYR_V1_DOMAIN_IDNA_ITERATOR_HPP
-#define SKYR_V1_DOMAIN_IDNA_ITERATOR_HPP
-
-#include <iterator>
-#include <skyr/v1/domain/errors.hpp>
-#include <skyr/v1/unicode/code_point.hpp>
-#include <skyr/v1/unicode/ranges/sentinel.hpp>
-#include "idna.hpp"
-
-namespace skyr {
-inline namespace v1 {
-namespace idna {
-/// An iterator adapter that converts a domain name.
-/// \tparam Iterator
-/// \tparam Sentinel
-template <
-    class Iterator,
-    class Sentinel=unicode::sentinel
-    >
-class idna_code_point_map_iterator {
- public:
-
-  using iterator_category = std::forward_iterator_tag;
-  using value_type = tl::expected<char32_t, domain_errc>;
-  using const_reference = value_type;
-  using reference = const_reference;
-  using const_pointer = const value_type *;
-  using pointer = const_pointer;
-  using difference_type = typename Iterator::difference_type;
-  using size_type = typename Iterator::size_type;
-
-  /// Constructor
-  /// \param first
-  /// \param last
-  /// \param use_std3_ascii_rules
-  /// \param transitional_processing
-  explicit constexpr idna_code_point_map_iterator(
-      Iterator first,
-      Sentinel last,
-      bool use_std3_ascii_rules,
-      bool transitional_processing)
-  : it_(first)
-  , last_(last)
-  , use_std3_ascii_rules_(use_std3_ascii_rules)
-  , transitional_processing_(transitional_processing) {}
-
-  ///
-  /// \return
-  constexpr auto operator ++ () noexcept -> idna_code_point_map_iterator & {
-    increment();
-    return *this;
-  }
-
-  ///
-  /// \return
-  constexpr auto operator ++ (int) noexcept -> idna_code_point_map_iterator {
-    auto result = *this;
-    increment();
-    return result;
-  }
-
-  ///
-  /// \return
-  constexpr auto operator * () const noexcept -> const_reference {
-    constexpr auto map_code_point = [] (char32_t code_point, bool use_std3_ascii_rules, bool transitional_processing)
-        -> tl::expected<char32_t, domain_errc> {
-      switch (idna::code_point_status(code_point)) {
-        case idna::idna_status::disallowed:
-          return tl::make_unexpected(domain_errc::disallowed_code_point);
-        case idna::idna_status::disallowed_std3_valid:
-          if (use_std3_ascii_rules) {
-            return tl::make_unexpected(domain_errc::disallowed_code_point);
-          } else {
-            return code_point;
-          }
-          break;
-        case idna::idna_status::disallowed_std3_mapped:
-          if (use_std3_ascii_rules) {
-            return tl::make_unexpected(domain_errc::disallowed_code_point);
-          } else {
-            return idna::map_code_point(code_point);
-          }
-        case idna::idna_status::ignored:
-          return tl::make_unexpected(domain_errc::disallowed_code_point);
-        case idna::idna_status::mapped:
-          return idna::map_code_point(code_point);
-        case idna::idna_status::deviation:
-          if (transitional_processing) {
-            return idna::map_code_point(code_point);
-          } else {
-            return code_point;
-          }
-        case idna::idna_status::valid:
-          return code_point;
-      }
-      return code_point;
-    };
-
-    auto code_point = unicode::u32_value(*it_);
-    if (code_point) {
-      return map_code_point(code_point.value(), use_std3_ascii_rules_, transitional_processing_);
-    }
-    else {
-      return tl::make_unexpected(domain_errc::encoding_error);
-    }
-  }
-
-  ///
-  /// \param sentinel
-  /// \return
-  [[nodiscard]] constexpr auto operator == ([[maybe_unused]] unicode::sentinel sentinel) const noexcept {
-    return it_ == last_;
-  }
-
-  ///
-  /// \param sentinel
-  /// \return
-  [[nodiscard]] constexpr auto operator != (unicode::sentinel sentinel) const noexcept {
-    return !(*this == sentinel);
-  }
-
- private:
-
-  constexpr void increment() {
-    constexpr auto is_ignored = [] (auto code_point) {
-      auto value = unicode::u32_value(code_point);
-      return (value && idna::code_point_status(value.value()) == idna::idna_status::ignored);
-    };
-
-    ++it_;
-    while ((it_ != last_) && is_ignored(*it_)) {
-      ++it_;
-    }
-  }
-
-  Iterator it_;
-  Sentinel last_;
-  bool use_std3_ascii_rules_;
-  bool transitional_processing_;
-
-};
-
-/// A range adapter that maps code points in a domain name using IDNA
-/// \tparam DomainName
-template <class DomainName>
-class idna_code_point_map_range {
- public:
-
-  ///
-  /// \param domain_name
-  /// \param use_std3_ascii_rules
-  /// \param transitional_processing
-  constexpr idna_code_point_map_range(
-      DomainName &&domain_name,
-      bool use_std3_ascii_rules,
-      bool transitional_processing)
-  : domain_name_(domain_name)
-  , use_std3_ascii_rules_(use_std3_ascii_rules)
-  , transitional_processing_(transitional_processing) {}
-
-  ///
-  /// \return
-  [[nodiscard]] constexpr auto cbegin() const noexcept {
-    return idna_code_point_map_iterator<unicode::traits::range_iterator_t<DomainName>>(
-        std::cbegin(domain_name_), std::cend(domain_name_), use_std3_ascii_rules_, transitional_processing_);
-  }
-
-  ///
-  /// \return
-  [[nodiscard]] constexpr auto cend() const noexcept {
-    return unicode::sentinel{};
-  }
-
-  ///
-  /// \return
-  [[nodiscard]] constexpr auto begin() const noexcept {
-    return cbegin();
-  }
-
-  ///
-  /// \return
-  [[nodiscard]] constexpr auto end() const noexcept {
-    return cend();
-  }
-
- private:
-  DomainName domain_name_;
-  bool use_std3_ascii_rules_;
-  bool transitional_processing_;
-
-};
-
-namespace views {
-///
-/// \tparam DomainName
-/// \param domain_name
-/// \param use_std3_ascii_rules
-/// \param transitional_processing
-/// \return A range adapter
-template <class DomainName>
-constexpr inline auto map_code_points(
-    DomainName &&domain_name, bool use_std3_ascii_rules, bool transitional_processing)
-    -> idna_code_point_map_range<DomainName> {
-  return {domain_name, use_std3_ascii_rules, transitional_processing};
-}
-}  // namespaec views
-}  // namespace idna
-}  // namespace v1
-}  // namespace skyr
-
-#endif  // SKYR_V1_DOMAIN_IDNA_ITERATOR_HPP
diff --git a/tests/domain/idna_table_tests.cpp b/tests/domain/idna_table_tests.cpp
index ebe221ed..2b0d4021 100644
--- a/tests/domain/idna_table_tests.cpp
+++ b/tests/domain/idna_table_tests.cpp
@@ -1,12 +1,12 @@
-// Copyright 2018-19 Glyn Matthews.
+// Copyright 2018-20 Glyn Matthews.
 // Distributed under the Boost Software License, Version 1.0.
 // (See accompanying file LICENSE_1_0.txt or copy at
 //  http://www.boost.org/LICENSE_1_0.txt)
 
 #define CATCH_CONFIG_MAIN
 #include <catch2/catch.hpp>
+#include <skyr/v1/domain/idna.hpp>
 
-#include "../../src/v1/domain/idna.hpp"
 
 TEST_CASE("IDNA character values", "[idna]") {
   using param = std::pair<char32_t, skyr::idna::idna_status>;
diff --git a/tests/domain/punycode_tests.cpp b/tests/domain/punycode_tests.cpp
index 262fa56c..374a6c89 100644
--- a/tests/domain/punycode_tests.cpp
+++ b/tests/domain/punycode_tests.cpp
@@ -6,7 +6,7 @@
 #include <string>
 #define CATCH_CONFIG_MAIN
 #include <catch2/catch.hpp>
-#include "../../src/v1/domain/punycode.hpp"
+#include <skyr/v1/domain/punycode.hpp>
 
 
 TEST_CASE("encode_test", "[punycode]") {
