diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,11 @@
 # Changelog and Acknowledgements
 
+## 1.1.5
+* Fixed [#17](https://github.com/sjshuck/pcre2/17), where functions returning
+  `Alternative` containers were not restricted to single results despite their
+  documentation.
+* Minor improvements to docs and examples.
+
 ## 1.1.4
 * Fixed some incorrect foreign imports' safety.
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -12,11 +12,19 @@
 ```
 ```haskell
 case "The quick brown fox" of
-    [regex|brown\s+(?<animal>\S+)|] -> Text.putStrLn animal
+    [regex|brown\s+(?<animal>\w+)|] -> Text.putStrLn animal
     _                               -> error "nothing brown"
 ```
 ```haskell
-let kv'd = lined . packed . [_regex|^\s*(.*?)\s*[=:]\s*(.*)|]
+let kv'd = lined . packed . [_regex|(?x)  # Extended PCRE2 syntax
+        ^\s*          # Ignore leading whitespace
+        ([^=:\s].*?)  # Capture the non-empty key
+        \s*           # Ignore trailing whitespace
+        [=:]          # Separator
+        \s*           # Ignore leading whitespace
+        (.*?)         # Capture the possibly-empty value
+        \s*$          # Ignore trailing whitespace
+    |]
 
 forMOf kv'd file $ execStateT $ do
     k <- gets $ capture @1
@@ -31,7 +39,6 @@
 ```
 
 ## Features
-* Low-surface API covering most use cases.
 * Quiet functions with simple types&mdash;for the most part it's  
 `Text` _(pattern)_ `-> Text` _(subject)_ `-> result`.
 * Use partial application to create performant, compile-once-match-many code.
diff --git a/bench/Bench.hs b/bench/Bench.hs
--- a/bench/Bench.hs
+++ b/bench/Bench.hs
@@ -1,7 +1,6 @@
 {-# LANGUAGE DataKinds #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE LambdaCase #-}
-{-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE ViewPatterns #-}
 
diff --git a/pcre2.cabal b/pcre2.cabal
--- a/pcre2.cabal
+++ b/pcre2.cabal
@@ -1,13 +1,13 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.33.0.
+-- This file has been generated from package.yaml by hpack version 0.34.4.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 31554f80cf0112dda9453046d82fbd24ca1c7090ffa1e4ff1f3709967ca2326f
+-- hash: cb8596609200fe862c181845b59caabfdf1c2a19dc0f410828ecba4f7c2c4fe5
 
 name:           pcre2
-version:        1.1.4
+version:        1.1.5
 synopsis:       Regular expressions via the PCRE2 C library (included)
 description:    Please see the README on GitHub at <https://github.com/sjshuck/hs-pcre2>
 category:       Text
diff --git a/src/c/getters.h b/src/c/getters.h
--- a/src/c/getters.h
+++ b/src/c/getters.h
@@ -8,7 +8,7 @@
 #ifdef GETTER
 #error GETTER already defined
 #endif
-#define GETTER(src, field, ret) \
+#define GETTER(src, ret, field) \
     ret pcre2_##src##_##field(pcre2_##src *block) \
     { \
         return block->field; \
@@ -22,38 +22,38 @@
         return block->version;
     }
 */
-GETTER(callout_block, version,               uint32_t)
-GETTER(callout_block, callout_number,        uint32_t)
-GETTER(callout_block, capture_top,           uint32_t)
-GETTER(callout_block, capture_last,          uint32_t)
-GETTER(callout_block, callout_flags,         uint32_t)
-GETTER(callout_block, offset_vector,         PCRE2_SIZE *)
-GETTER(callout_block, mark,                  PCRE2_SPTR)
-GETTER(callout_block, subject,               PCRE2_SPTR)
-GETTER(callout_block, subject_length,        PCRE2_SIZE)
-GETTER(callout_block, start_match,           PCRE2_SIZE)
-GETTER(callout_block, current_position,      PCRE2_SIZE)
-GETTER(callout_block, pattern_position,      PCRE2_SIZE)
-GETTER(callout_block, next_item_length,      PCRE2_SIZE)
-GETTER(callout_block, callout_string_offset, PCRE2_SIZE)
-GETTER(callout_block, callout_string_length, PCRE2_SIZE)
-GETTER(callout_block, callout_string,        PCRE2_SPTR)
+GETTER(callout_block, uint32_t,     version)
+GETTER(callout_block, uint32_t,     callout_number)
+GETTER(callout_block, uint32_t,     capture_top)
+GETTER(callout_block, uint32_t,     capture_last)
+GETTER(callout_block, uint32_t,     callout_flags)
+GETTER(callout_block, PCRE2_SIZE *, offset_vector)
+GETTER(callout_block, PCRE2_SPTR,   mark)
+GETTER(callout_block, PCRE2_SPTR,   subject)
+GETTER(callout_block, PCRE2_SIZE,   subject_length)
+GETTER(callout_block, PCRE2_SIZE,   start_match)
+GETTER(callout_block, PCRE2_SIZE,   current_position)
+GETTER(callout_block, PCRE2_SIZE,   pattern_position)
+GETTER(callout_block, PCRE2_SIZE,   next_item_length)
+GETTER(callout_block, PCRE2_SIZE,   callout_string_offset)
+GETTER(callout_block, PCRE2_SIZE,   callout_string_length)
+GETTER(callout_block, PCRE2_SPTR,   callout_string)
 
-GETTER(callout_enumerate_block, version,               uint32_t)
-GETTER(callout_enumerate_block, pattern_position,      PCRE2_SIZE)
-GETTER(callout_enumerate_block, next_item_length,      PCRE2_SIZE)
-GETTER(callout_enumerate_block, callout_number,        uint32_t)
-GETTER(callout_enumerate_block, callout_string_offset, PCRE2_SIZE)
-GETTER(callout_enumerate_block, callout_string_length, PCRE2_SIZE)
-GETTER(callout_enumerate_block, callout_string,        PCRE2_SPTR)
+GETTER(callout_enumerate_block, uint32_t,   version)
+GETTER(callout_enumerate_block, PCRE2_SIZE, pattern_position)
+GETTER(callout_enumerate_block, PCRE2_SIZE, next_item_length)
+GETTER(callout_enumerate_block, uint32_t,   callout_number)
+GETTER(callout_enumerate_block, PCRE2_SIZE, callout_string_offset)
+GETTER(callout_enumerate_block, PCRE2_SIZE, callout_string_length)
+GETTER(callout_enumerate_block, PCRE2_SPTR, callout_string)
 
-GETTER(substitute_callout_block, version,        uint32_t)
-GETTER(substitute_callout_block, subscount,      uint32_t)
-GETTER(substitute_callout_block, input,          PCRE2_SPTR)
-GETTER(substitute_callout_block, output,         PCRE2_SPTR)
-GETTER(substitute_callout_block, ovector,        PCRE2_SIZE *)
-GETTER(substitute_callout_block, oveccount,      uint32_t)
-GETTER(substitute_callout_block, output_offsets, PCRE2_SIZE *) // array of 2
+GETTER(substitute_callout_block, uint32_t,     version)
+GETTER(substitute_callout_block, uint32_t,     subscount)
+GETTER(substitute_callout_block, PCRE2_SPTR,   input)
+GETTER(substitute_callout_block, PCRE2_SPTR,   output)
+GETTER(substitute_callout_block, PCRE2_SIZE *, ovector)
+GETTER(substitute_callout_block, uint32_t,     oveccount)
+GETTER(substitute_callout_block, PCRE2_SIZE *, output_offsets) // array of 2
 
 #undef GETTER
 
diff --git a/src/hs/Text/Regex/Pcre2.hs b/src/hs/Text/Regex/Pcre2.hs
--- a/src/hs/Text/Regex/Pcre2.hs
+++ b/src/hs/Text/Regex/Pcre2.hs
@@ -231,9 +231,6 @@
     -- +--------------------+---------------------------------------+------------------------------+
     -- | @QuasiQuotes@      | @[@/f/@|@...@|]@ syntax               | Always                       |
     -- +--------------------+---------------------------------------+------------------------------+
-    -- | @TemplateHaskell@  | Running PCRE2 at compile time and     | Always                       |
-    -- |                    | generating code                       |                              |
-    -- +--------------------+---------------------------------------+------------------------------+
     -- | @TypeApplications@ | @\@i@ syntax for supplying type index | Using `capture`\/`_capture`  |
     -- |                    | arguments to applicable functions     |                              |
     -- +--------------------+---------------------------------------+------------------------------+
diff --git a/src/hs/Text/Regex/Pcre2/Internal.hs b/src/hs/Text/Regex/Pcre2/Internal.hs
--- a/src/hs/Text/Regex/Pcre2/Internal.hs
+++ b/src/hs/Text/Regex/Pcre2/Internal.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE AllowAmbiguousTypes #-}
 {-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveFunctor #-}
 {-# LANGUAGE ExistentialQuantification #-}
 {-# LANGUAGE FunctionalDependencies #-}
 {-# LANGUAGE LambdaCase #-}
@@ -21,14 +22,13 @@
 import           Data.Function              ((&))
 import           Data.Functor               ((<&>))
 import           Data.Functor.Const         (Const(..))
-import           Data.Functor.Identity      (Identity(..))
 import           Data.IORef
 import           Data.IntMap.Strict         (IntMap)
 import qualified Data.IntMap.Strict         as IM
 import           Data.List                  (foldl', intercalate)
 import           Data.List.NonEmpty         (NonEmpty(..))
 import qualified Data.List.NonEmpty         as NE
-import           Data.Monoid
+import           Data.Monoid                (Alt(..), Any(..), Endo(..))
 import           Data.Proxy                 (Proxy(..))
 import           Data.Text                  (Text)
 import qualified Data.Text                  as Text
@@ -42,7 +42,7 @@
 import qualified Foreign.Concurrent         as Conc
 import           GHC.TypeLits               hiding (Text)
 import qualified GHC.TypeLits               as TypeLits
-import           System.IO.Unsafe
+import           System.IO.Unsafe           (unsafePerformIO)
 import           Text.Regex.Pcre2.Foreign
 
 -- * General utilities
@@ -70,10 +70,6 @@
 bitOr :: (Foldable t, Bits a) => t a -> a
 bitOr = foldl' (.|.) zeroBits
 
--- | Placeholder for half-building a `Traversal'` to be passed to `has`.
-noTouchy :: a
-noTouchy = error "BUG! Tried to use match results"
-
 -- | Equivalent to @flip fix@.
 --
 -- Used to express a recursive function of one argument that is called only once
@@ -107,8 +103,8 @@
 thinSlice text (SliceRange off offEnd)
     | off == fromIntegral pcre2_UNSET = Text.empty
     | otherwise                       = text
+        & Text.takeWord16 offEnd
         & Text.dropWord16 off
-        & Text.takeWord16 (offEnd - off)
 
 -- | Slice a 'Text', copying if it\'s less than half of the original.
 slice :: Text -> SliceRange -> Text
@@ -127,25 +123,37 @@
 instance CastCUs Word16 CUShort
 
 -- ** Lens types and utilities
---
--- $LensTypesAndUtilities
--- The combinators\' types are excessively polymorphic\/polykinded\/ugly because
--- they have no accompanying signatures (not worth the trouble).
 
 type Lens'      s a = forall f. (Functor f)     => (a -> f a) -> s -> f s
 type Traversal' s a = forall f. (Applicative f) => (a -> f a) -> s -> f s
 
-preview l = getFirst . getConst . l (Const . First . Just)
+type Getting r s a = (a -> Const r a) -> s -> Const r s
+
+preview :: Getting (Alt Maybe a) s a -> s -> Maybe a
+preview = toAlternativeOf
+
+view :: Getting a s a -> s -> a
 view l = getConst . l Const
+
+to :: (s -> a) -> forall r. Getting r s a
 to k f = Const . getConst . f . k
+
+has :: Getting Any s a -> s -> Bool
 has l = getAny . getConst . l (\_ -> Const $ Any True)
-toListOf l = flip appEndo [] . view (l . to (Endo . (:)))
 
--- toAlternativeOf :: (Alternative f) => Getting (Alt f a) s a -> s -> f a
-toAlternativeOf l = getAlt . view (l . to (Alt . pure))
+toListOf :: Getting (Endo [a]) s a -> s -> [a]
+toListOf l x = let build = Endo . (:) in view (l . to build) x `appEndo` []
 
+toAlternativeOf :: (Alternative f) => Getting (Alt f a) s a -> s -> f a
+toAlternativeOf l = let alt = Alt . pure in getAlt . view (l . to alt)
+
+-- | See https://github.com/sjshuck/hs-pcre2/issues/17.
+-- This should go away with https://github.com/sjshuck/hs-pcre2/issues/18.
+toAlternativeOf1 :: (Alternative f) => Getting (Alt Maybe a) s a -> s -> f a
+toAlternativeOf1 l = maybe empty pure . preview l
+
 _headNE :: Lens' (NonEmpty a) a
-_headNE f (x :| xs) = f x <&> \x' -> x' :| xs
+_headNE f (x :| xs) = f x <&> (:| xs)
 
 -- ** Streaming support
 
@@ -155,12 +163,7 @@
     | StreamYield b (Stream b m a)    -- ^ yield a value and keep going
     | StreamEffect (m (Stream b m a)) -- ^ have an effect and keep going
     | StreamStop                      -- ^ short-circuit
-
-instance (Functor m) => Functor (Stream b m) where
-    f `fmap` StreamPure x     = StreamPure $ f x
-    f `fmap` StreamYield y sx = StreamYield y $ f <$> sx
-    f `fmap` StreamEffect msx = StreamEffect $ fmap f <$> msx
-    _ `fmap` StreamStop       = StreamStop
+    deriving (Functor)
 
 instance (Functor m) => Applicative (Stream b m) where
     pure = StreamPure
@@ -170,7 +173,6 @@
     StreamStop       <*> _  = StreamStop
 
 instance (Functor m) => Monad (Stream b m) where
-    return = pure
     StreamPure x     >>= f = f x
     StreamYield y sx >>= f = StreamYield y $ sx >>= f
     StreamEffect msx >>= f = StreamEffect $ msx <&> (>>= f)
@@ -328,10 +330,10 @@
     | SubReplacementOnly -- ^ /Affects `subOpt`./  Return just the rendered
     -- replacement instead of it within the subject.  With `SubGlobal`, all
     -- results are concatenated.
-    | SubUnknownUnset -- ^ /Affects `subOpt`./  References in the replacement
-    -- to non-existent captures don\'t error but are treated as unset.
-    | SubUnsetEmpty -- ^ /Affects `subOpt`./  References in the replacement
-    -- to unset captures don\'t error but are treated as empty.
+    | SubUnknownUnset -- ^ /Affects `subOpt`./  References in the replacement to
+    -- non-existent captures don\'t error but are treated as unset.
+    | SubUnsetEmpty -- ^ /Affects `subOpt`./  References in the replacement to
+    -- unset captures don\'t error but are treated as empty.
     | Ucp -- ^ Count Unicode characters in some character classes such as @\\d@.
     -- Incompatible with `NeverUcp`.
     | Ungreedy -- ^ Invert the effect of @?@.  Without it, quantifiers are
@@ -489,11 +491,11 @@
     TwoOptions opt0 opt1 -> applyOption opt0 ++ applyOption opt1
 
     -- CompileOption
-    Anchored          -> [CompileOption pcre2_ANCHORED]
     AllowEmptyClass   -> [CompileOption pcre2_ALLOW_EMPTY_CLASS]
     AltBsuxLegacy     -> [CompileOption pcre2_ALT_BSUX]
     AltCircumflex     -> [CompileOption pcre2_ALT_CIRCUMFLEX]
     AltVerbNames      -> [CompileOption pcre2_ALT_VERBNAMES]
+    Anchored          -> [CompileOption pcre2_ANCHORED]
     AutoCallout       -> [CompileOption pcre2_AUTO_CALLOUT]
     Caseless          -> [CompileOption pcre2_CASELESS]
     DollarEndOnly     -> [CompileOption pcre2_DOLLAR_ENDONLY]
@@ -538,14 +540,14 @@
 
     -- MatchOption
     NotBol             -> [MatchOption pcre2_NOTBOL]
-    NotEol             -> [MatchOption pcre2_NOTEOL]
     NotEmpty           -> [MatchOption pcre2_NOTEMPTY]
     NotEmptyAtStart    -> [MatchOption pcre2_NOTEMPTY_ATSTART]
+    NotEol             -> [MatchOption pcre2_NOTEOL]
     PartialHard        -> [MatchOption pcre2_PARTIAL_HARD]
     PartialSoft        -> [MatchOption pcre2_PARTIAL_SOFT]
-    SubReplacementOnly -> [MatchOption pcre2_SUBSTITUTE_REPLACEMENT_ONLY]
     SubGlobal          -> [MatchOption pcre2_SUBSTITUTE_GLOBAL]
     SubLiteral         -> [MatchOption pcre2_SUBSTITUTE_LITERAL]
+    SubReplacementOnly -> [MatchOption pcre2_SUBSTITUTE_REPLACEMENT_ONLY]
     SubUnknownUnset    -> [MatchOption pcre2_SUBSTITUTE_UNKNOWN_UNSET]
     SubUnsetEmpty      -> [MatchOption pcre2_SUBSTITUTE_UNSET_EMPTY]
 
@@ -556,14 +558,14 @@
     UnsafeSubCallout f -> [SubCalloutOption f]
 
     -- MatchContextOption
-    OffsetLimit limit -> CompileOption pcre2_USE_OFFSET_LIMIT : unary
-        MatchContextOption pcre2_set_offset_limit (fromIntegral limit)
+    DepthLimit limit -> unary
+        MatchContextOption pcre2_set_depth_limit (fromIntegral limit)
     HeapLimit limit -> unary
         MatchContextOption pcre2_set_heap_limit (fromIntegral limit)
     MatchLimit limit -> unary
         MatchContextOption pcre2_set_match_limit (fromIntegral limit)
-    DepthLimit limit -> unary
-        MatchContextOption pcre2_set_depth_limit (fromIntegral limit)
+    OffsetLimit limit -> CompileOption pcre2_USE_OFFSET_LIMIT : unary
+        MatchContextOption pcre2_set_offset_limit (fromIntegral limit)
 
     where
     unary ctor f x = (: []) $ ctor $ \ctx -> withForeignPtr ctx $ \ctxPtr ->
@@ -608,7 +610,7 @@
 type ExtractOpts = StateT [AppliedOption] IO
 
 -- | Use a fake @Prism'@ to extract a category of options.
-extractOptsOf :: Traversal' AppliedOption a -> ExtractOpts [a]
+extractOptsOf :: Getting (Alt Maybe a) AppliedOption a -> ExtractOpts [a]
 extractOptsOf traversal = state $ partitionEithers . map discrim where
     discrim opt = maybe (Right opt) Left $ preview traversal opt
 
@@ -719,11 +721,7 @@
 
 -- | Helper for @assemble*@ functions.  Basically, extract all options and help
 -- produce a function that takes a `Text` subject.
-assembleSubjFun
-    :: (MatchEnv -> Text -> a)
-    -> Option
-    -> Text
-    -> IO (Text -> a)
+assembleSubjFun :: (MatchEnv -> Text -> a) -> Option -> Text -> IO (Text -> a)
 assembleSubjFun mkSubjFun option patt =
     runStateT extractAll (applyOption option) <&> \case
         (subjFun, []) -> subjFun
@@ -739,46 +737,44 @@
 
 -- | Produce a `Matcher` from user-supplied `Option` and pattern.
 assembleMatcher :: Option -> Text -> IO Matcher
-assembleMatcher = assembleSubjFun $ \matchEnv@(MatchEnv {..}) ->
-    let matchTempEnvWithSubj = mkMatchTempEnv matchEnv
-    in \subject -> StreamEffect $
-        withForeignPtr matchEnvCode $ \codePtr ->
-        withMatchDataFromCode codePtr $ \matchDataPtr ->
-        Text.useAsPtr subject $ \subjPtr subjCUs -> do
-            MatchTempEnv {..} <- matchTempEnvWithSubj subject
+assembleMatcher = assembleSubjFun $ \matchEnv@(MatchEnv {..}) subject ->
+    StreamEffect $
+    withForeignPtr matchEnvCode $ \codePtr ->
+    withMatchDataFromCode codePtr $ \matchDataPtr ->
+    Text.useAsPtr subject $ \subjPtr subjCUs -> do
+        MatchTempEnv {..} <- mkMatchTempEnv matchEnv subject
 
-            -- Loop over the subject, emitting slice lists until stopping.
-            return $ fix1 0 $ \continue curOff -> do
-                result <- liftIO $
-                    withForeignOrNullPtr matchTempEnvCtx $ \ctxPtr ->
-                        pcre2_match
-                            codePtr
-                            (castCUs subjPtr)
-                            (fromIntegral subjCUs)
-                            curOff
-                            matchEnvOpts
-                            matchDataPtr
-                            ctxPtr
+        -- Loop over the subject, emitting match data until stopping.
+        return $ fix1 0 $ \continue curOff -> do
+            result <- liftIO $ withForeignOrNullPtr matchTempEnvCtx $ \ctxPtr ->
+                pcre2_match
+                    codePtr
+                    (castCUs subjPtr)
+                    (fromIntegral subjCUs)
+                    curOff
+                    matchEnvOpts
+                    matchDataPtr
+                    ctxPtr
 
-                -- Handle no match and errors
-                when (result == pcre2_ERROR_NOMATCH) StreamStop
-                when (result == pcre2_ERROR_CALLOUT) $
-                    liftIO $ maybeRethrow matchTempEnvRef
-                liftIO $ check (> 0) result
+            -- Handle no match and errors
+            when (result == pcre2_ERROR_NOMATCH) StreamStop
+            when (result == pcre2_ERROR_CALLOUT) $
+                liftIO $ maybeRethrow matchTempEnvRef
+            liftIO $ check (> 0) result
 
-                streamYield matchDataPtr
+            streamYield matchDataPtr
 
-                -- Determine next starting offset
-                nextOff <- liftIO $ do
-                    ovecPtr <- pcre2_get_ovector_pointer matchDataPtr
-                    curOffEnd <- peekElemOff ovecPtr 1
-                    -- Prevent infinite loop upon empty match
-                    return $ max curOffEnd (curOff + 1)
+            -- Determine next starting offset
+            nextOff <- liftIO $ do
+                ovecPtr <- pcre2_get_ovector_pointer matchDataPtr
+                curOffEnd <- peekElemOff ovecPtr 1
+                -- Prevent infinite loop upon empty match
+                return $ max curOffEnd (curOff + 1)
 
-                -- Handle end of subject
-                when (nextOff > fromIntegral subjCUs) StreamStop
+            -- Handle end of subject
+            when (nextOff > fromIntegral subjCUs) StreamStop
 
-                continue nextOff
+            continue nextOff
 
     where
     withMatchDataFromCode codePtr action = do
@@ -807,8 +803,7 @@
     -> Option
     -> Text      -- ^ pattern
     -> IO Subber
-assembleSubber replacement =
-    assembleSubjFun $ \firstMatchEnv@(MatchEnv {..}) ->
+assembleSubber replacement = assembleSubjFun $ \firstMatchEnv@(MatchEnv {..}) ->
     -- Subber
     \subject ->
     withForeignPtr matchEnvCode $ \codePtr ->
@@ -882,9 +877,9 @@
     :: MatchEnv
     -> Text -- ^ Callout info requires access to the original subject.
     -> IO MatchTempEnv
-mkMatchTempEnv (MatchEnv {..})
-    | null matchEnvCallout && null matchEnvSubCallout = \_ -> return noCallouts
-    | otherwise                                       = \subject -> do
+mkMatchTempEnv (MatchEnv {..}) subject
+    | null matchEnvCallout && null matchEnvSubCallout = return noCallouts
+    | otherwise                                       = do
         calloutStateRef <- newIORef $ CalloutState {
             calloutStateException = Nothing,
             calloutStateSubsLog   = IM.empty}
@@ -981,7 +976,7 @@
 -- data to Haskell and present to the user function.  Ensure no pointers are
 -- leaked!
 getCalloutInfo :: Text -> Ptr Pcre2_callout_block -> IO CalloutInfo
-getCalloutInfo subject blockPtr = do
+getCalloutInfo calloutSubject blockPtr = do
     calloutIndex <- do
         str <- pcre2_callout_block_callout_string blockPtr
         if str == nullPtr
@@ -998,20 +993,16 @@
                 len <- pcre2_callout_block_callout_string_length blockPtr
                 CalloutName <$> Text.fromPtr (castCUs str) (fromIntegral len)
 
-    let calloutSubject = subject
-
     calloutCaptures <- do
         ovecPtr <- pcre2_callout_block_offset_vector blockPtr
         top <- pcre2_callout_block_capture_top blockPtr
         forM (0 :| [1 .. fromIntegral top - 1]) $ \n -> do
-            start <- peekElemOff ovecPtr $ n * 2
-            if start == pcre2_UNSET
-                then return Nothing
-                else Just <$> do
-                    end <- peekElemOff ovecPtr $ n * 2 + 1
-                    return $ slice calloutSubject $ SliceRange
-                        (fromIntegral start)
-                        (fromIntegral end)
+            [start, end] <- forM [0, 1] $ peekElemOff ovecPtr . (n * 2 +)
+            return $ if start == pcre2_UNSET
+                then Nothing
+                else Just $ slice calloutSubject $ SliceRange
+                    (fromIntegral start)
+                    (fromIntegral end)
 
     calloutMark <- do
         ptr <- pcre2_callout_block_mark blockPtr
@@ -1045,14 +1036,12 @@
         ovecPtr <- pcre2_substitute_callout_block_ovector blockPtr
         ovecCount <- pcre2_substitute_callout_block_oveccount blockPtr
         forM (0 :| [1 .. fromIntegral ovecCount - 1]) $ \n -> do
-            start <- peekElemOff ovecPtr $ n * 2
-            if start == pcre2_UNSET
-                then return Nothing
-                else Just <$> do
-                    end <- peekElemOff ovecPtr $ n * 2 + 1
-                    return $ slice subCalloutSubject $ SliceRange
-                        (fromIntegral start)
-                        (fromIntegral end)
+            [start, end] <- forM [0, 1] $ peekElemOff ovecPtr . (n * 2 +)
+            return $ if start == pcre2_UNSET
+                then Nothing
+                else Just $ slice subCalloutSubject $ SliceRange
+                    (fromIntegral start)
+                    (fromIntegral end)
 
     subCalloutReplacement <- do
         outPtr <- pcre2_substitute_callout_block_output blockPtr
@@ -1074,12 +1063,8 @@
 -- @Setter'@ to perform substitutions at the Haskell level.  Operates globally.
 --
 -- Internal only!  Users should not (have to) know about `Matcher`.
-_capturesInternal
-    :: Matcher
-    -> FromMatch                    -- ^ get some or all captures offsets
-    -> (Text -> SliceRange -> Text) -- ^ slicer
-    -> Traversal' Text (NonEmpty Text)
-_capturesInternal matcher getSliceRanges slicer f subject =
+_capturesInternal :: Matcher -> FromMatch -> Traversal' Text (NonEmpty Text)
+_capturesInternal matcher fromMatch f subject =
     traverse f captureLists <&> \captureLists' ->
         -- Swag foldl-as-foldr to create only as many segments as we need to
         -- stitch back together and no more.
@@ -1090,9 +1075,8 @@
         in Text.concat $ foldr mkSegments termSegments triples 0
 
     where
-    sliceRangeLists = unsafeLazyStreamToList $
-        mapMS getSliceRanges $ matcher subject
-    captureLists = map (NE.map $ slicer subject) sliceRangeLists
+    sliceRangeLists = unsafeLazyStreamToList $ mapMS fromMatch $ matcher subject
+    captureLists = map (NE.map $ slice subject) sliceRangeLists
 
     mkSegments (SliceRange off offEnd, c, c') r prevOffEnd
         | off == fromIntegral pcre2_UNSET || c == c' =
@@ -1138,10 +1122,9 @@
 
     getWhitelistedSliceRanges whitelist matchDataPtr
 
--- | Helper to create non-Template Haskell API functions.  They all take options
--- and a pattern, and then do something via a 'Matcher'.
-withMatcher :: (Matcher -> a) -> Option -> Text -> a
-withMatcher f option patt = f $ unsafePerformIO $ assembleMatcher option patt
+-- | Placeholder for half-building a `Traversal'` to be passed to `has`.
+errorFromMatch :: FromMatch
+errorFromMatch _ = return $ error "BUG! Tried to use match results"
 
 -- | Match a pattern to a subject once and return a list of captures, or @[]@ if
 -- no match.
@@ -1150,8 +1133,7 @@
 
 -- | @capturesOpt mempty = captures@
 capturesOpt :: Option -> Text -> Text -> [Text]
-capturesOpt option patt =
-    maybe [] NE.toList . preview (_capturesOpt option patt)
+capturesOpt option patt = maybe [] NE.toList . capturesAOpt option patt
 
 -- | Match a pattern to a subject once and return a non-empty list of captures
 -- in an `Alternative`, or `empty` if no match.  The non-empty list constructor
@@ -1168,7 +1150,7 @@
 --
 -- @since 1.1.0
 capturesAOpt :: (Alternative f) => Option -> Text -> Text -> f (NonEmpty Text)
-capturesAOpt option patt = toAlternativeOf $ _capturesOpt option patt
+capturesAOpt option patt = toAlternativeOf1 $ _capturesOpt option patt
 
 -- | Match a pattern to a subject and lazily produce a list of all
 -- non-overlapping portions, with all capture groups, that matched.
@@ -1189,10 +1171,8 @@
 
 -- | @matchesOpt mempty = matches@
 matchesOpt :: Option -> Text -> Text -> Bool
-matchesOpt = withMatcher $ \matcher -> has $ _capturesInternal
-    matcher
-    (const $ return noTouchy)
-    noTouchy
+matchesOpt option patt = has $ _capturesInternal matcher errorFromMatch where
+    matcher = unsafePerformIO $ assembleMatcher option patt
 
 -- | Match a pattern to a subject once and return the portion that matched in an
 -- `Alternative`, or `empty` if no match.
@@ -1201,7 +1181,7 @@
 
 -- | @matchOpt mempty = match@
 matchOpt :: (Alternative f) => Option -> Text -> Text -> f Text
-matchOpt option patt = toAlternativeOf $ _matchOpt option patt
+matchOpt option patt = toAlternativeOf1 $ _matchOpt option patt
 
 -- | Match a pattern to a subject and lazily return a list of all
 -- non-overlapping portions that matched.
@@ -1286,8 +1266,8 @@
 
 -- | @_capturesOpt mempty = _captures@
 _capturesOpt :: Option -> Text -> Traversal' Text (NonEmpty Text)
-_capturesOpt = withMatcher $ \matcher ->
-    _capturesInternal matcher getAllSliceRanges slice
+_capturesOpt option patt = _capturesInternal matcher getAllSliceRanges where
+    matcher = unsafePerformIO $ assembleMatcher option patt
 
 -- | Given a pattern, produce a traversal (0 or more targets) that focuses from
 -- a subject to the portions of it that match.
@@ -1298,12 +1278,13 @@
 
 -- | @_matchOpt mempty = _match@
 _matchOpt :: Option -> Text -> Traversal' Text Text
-_matchOpt = withMatcher $ \matcher ->
-    _capturesInternal matcher get0thSliceRanges slice . _headNE
+_matchOpt option patt = _cs . _headNE where
+    _cs = _capturesInternal matcher get0thSliceRanges
+    matcher = unsafePerformIO $ assembleMatcher option patt
 
 -- * Support for Template Haskell compile-time regex analysis
 
--- | From options and pattern, determine parenthesized patterns\' names in
+-- | From options and pattern, determine parenthesized captures\' names in
 -- order.
 predictCaptureNames :: Option -> Text -> IO [Maybe Text]
 predictCaptureNames option patt = do
@@ -1313,7 +1294,7 @@
 
     withForeignPtr code getCaptureNames
 
--- | NOTE: The 0th capture is always named @Nothing@.
+-- | Get parenthesized captures\' names in order.
 getCaptureNames :: Ptr Pcre2_code -> IO [Maybe Text]
 getCaptureNames codePtr = do
     nameCount <- getCodeInfo @CUInt codePtr pcre2_INFO_NAMECOUNT
@@ -1334,7 +1315,7 @@
 
     hiCaptNum <- getCodeInfo @CUInt codePtr pcre2_INFO_CAPTURECOUNT
 
-    return $ map (names IM.!?) [0 .. fromIntegral hiCaptNum]
+    return $ map (names IM.!?) [1 .. fromIntegral hiCaptNum]
 
 -- | Low-level access to compiled pattern info, per the docs.
 getCodeInfo :: (Storable a) => Ptr Pcre2_code -> CUInt -> IO a
diff --git a/src/hs/Text/Regex/Pcre2/TH.hs b/src/hs/Text/Regex/Pcre2/TH.hs
--- a/src/hs/Text/Regex/Pcre2/TH.hs
+++ b/src/hs/Text/Regex/Pcre2/TH.hs
@@ -4,10 +4,8 @@
 
 module Text.Regex.Pcre2.TH where
 
-import           Control.Applicative        (Alternative(..))
-import           Data.Functor               ((<&>))
 import           Data.IORef
-import           Data.List.NonEmpty         (NonEmpty(..))
+import           Data.List.NonEmpty         (NonEmpty)
 import qualified Data.List.NonEmpty         as NE
 import           Data.Map.Lazy              (Map)
 import qualified Data.Map.Lazy              as Map
@@ -19,9 +17,13 @@
 import           System.IO.Unsafe           (unsafePerformIO)
 import           Text.Regex.Pcre2.Internal
 
+-- | Unexported, top-level `IORef` that\'s created upon the first runtime
+-- evaluation of a Template Haskell `Matcher`.
 globalMatcherCache :: IORef (Map Text Matcher)
 globalMatcherCache = unsafePerformIO $ newIORef Map.empty
+{-# NOINLINE globalMatcherCache #-}
 
+-- | Given a `Text`, create or retrieve a `Matcher` from the global cache.
 memoMatcher :: Text -> Matcher
 memoMatcher patt = unsafePerformIO $ do
     cache <- readIORef globalMatcherCache
@@ -33,27 +35,32 @@
                 (Map.insert patt matcher cache, ())
             return matcher
 
+-- | Generate code to produce \(and memoize\) a `Matcher` from a pattern.
 matcherQ :: String -> ExpQ
 matcherQ s = [e| memoMatcher $ Text.pack $(stringE s) |]
 
+-- | Predict parenthesized captures \(maybe named\) of a pattern at splice time.
 predictCaptureNamesQ :: String -> Q [Maybe Text]
 predictCaptureNamesQ = runIO . predictCaptureNames mempty . Text.pack
 
+-- | Get the indexes of `Just` the named captures.
 toKVs :: [Maybe Text] -> [(Int, Text)]
-toKVs names = [(number, name) | (number, Just name) <- zip [0 ..] names]
+toKVs names = [(number, name) | (number, Just name) <- zip [1 ..] names]
 
+-- | Generate the data-kinded phantom type parameter of `Captures` of a pattern,
+-- if needed.
 capturesInfoQ :: String -> Q (Maybe Type)
 capturesInfoQ s = predictCaptureNamesQ s >>= \case
-    -- No named captures, so need for Captures, so no info.
-    [Nothing] -> return Nothing
+    -- No parenthesized captures, so need for Captures, so no info.
+    [] -> return Nothing
 
-    -- Named captures.  Present
-    --     [Nothing, Just "foo", Just "bar", Nothing]
+    -- One or more parenthesized captures.  Present
+    --     [Just "foo", Just "bar", Nothing]
     -- as
     --     '(3, '[ '("foo", 1), '("bar", 2)]).
     captureNames -> Just <$> promotedTupleT 2 `appT` hi `appT` kvs where
         -- 3
-        hi = litT $ numTyLit $ fromIntegral $ length captureNames - 1
+        hi = litT $ numTyLit $ fromIntegral $ length captureNames
         -- '[ '("foo", 1), '("bar", 2)]
         kvs = foldr f promotedNilT $ toKVs captureNames where
             -- '("foo", 1) ': ...
@@ -82,13 +89,13 @@
 -- >             year = read @Int $ Text.unpack $ capture @"y" cs
 -- >             ...
 --
--- > forM_ ([regex|^\s+$|] line :: Maybe Text) $ \spaces ->
--- >     error $ "line has spaces only: " ++ show spaces
+-- > forM_ ([regex|\s+$|] line :: Maybe Text) $ \spaces -> error $
+-- >     "line has trailing spaces (" ++ show (Text.length spaces) ++ " characters)"
 --
 -- /__As a pattern__/
 --
--- This matches when the regex matches, whereupon any named captures are bound
--- to variables of the same names.
+-- This matches when the regex first matches, whereupon any named captures are
+-- bound to variables of the same names.
 --
 -- > case "submitted 2020-10-20" of
 -- >     [regex|(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})|] ->
@@ -101,15 +108,15 @@
 -- If there are no named captures, this simply acts as a guard.
 regex :: QuasiQuoter
 regex = QuasiQuoter {
-    quoteExp = \s -> capturesInfoQ s >>= \case
-        Nothing -> [e|
-            let _cs = _capturesInternal $(matcherQ s) get0thSliceRanges slice
-            in toAlternativeOf $ _cs . _headNE |]
-        Just info -> [e|
-            let _cs = _capturesInternal $(matcherQ s) getAllSliceRanges slice
-                wrap cs = Captures cs :: Captures $(return info)
-            in toAlternativeOf $ _cs . to wrap |],
+    quoteExp = \s -> do
+        let fromCsQ = capturesInfoQ s >>= maybe [e| _headNE |] toWrapQ
+            toWrapQ info = [e|
+                let wrap cs = Captures cs :: Captures $(return info)
+                in to wrap |]
 
+        [e| toAlternativeOf1 $
+            _capturesInternal $(matcherQ s) getAllSliceRanges . $(fromCsQ) |],
+
     quotePat = \s -> do
         captureNames <- predictCaptureNamesQ s
 
@@ -117,11 +124,7 @@
             -- No named captures.  Test whether the string matches without
             -- creating any new Text values.
             Nothing -> viewP
-                [e|
-                    has $ _capturesInternal
-                        $(matcherQ s)
-                        (const $ return noTouchy)
-                        noTouchy |]
+                [e| has $ _capturesInternal $(matcherQ s) errorFromMatch |]
                 [p| True |]
 
             -- One or more named captures.  Attempt to bind only those to local
@@ -132,7 +135,6 @@
                     let _cs = _capturesInternal
                             $(matcherQ s)
                             (getWhitelistedSliceRanges $(liftData numbers))
-                            slice
                     in view $ _cs . to NE.toList |]
                 p = foldr f wildP names where
                     f name r = conP '(:) [varP $ mkName $ Text.unpack name, r],
@@ -159,15 +161,14 @@
 --
 _regex :: QuasiQuoter
 _regex = QuasiQuoter {
-    quoteExp = \s -> capturesInfoQ s >>= \case
-        Nothing -> [e|
-            let _cs = _capturesInternal $(matcherQ s) get0thSliceRanges slice
-            in _cs . _headNE |]
-        Just info -> [e|
-            let _cs = _capturesInternal $(matcherQ s) getAllSliceRanges slice
-                wrapped :: Lens' (NonEmpty Text) (Captures $(return info))
-                wrapped f cs = f (Captures cs) <&> \(Captures cs') -> cs'
-            in _cs . wrapped |],
+    quoteExp = \s -> do
+        let fromCsQ = capturesInfoQ s >>= maybe [e| _headNE |] wrappedQ
+            wrappedQ info = [e|
+                let wrapped :: Lens' (NonEmpty Text) (Captures $(return info))
+                    wrapped f cs = f (Captures cs) <&> \(Captures cs') -> cs'
+                in wrapped |]
+
+        [e| _capturesInternal $(matcherQ s) getAllSliceRanges . $(fromCsQ) |],
 
     quotePat = const $ fail "_regex: cannot produce a pattern",
 
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -2,7 +2,6 @@
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE TemplateHaskell #-}
 {-# LANGUAGE TypeApplications #-}
 {-# LANGUAGE ViewPatterns #-}
 
@@ -222,9 +221,12 @@
             _                  ->
                 expectationFailure "quasi-quoted pattern didn't match"
 
+    issue 17 $ do
+        match "\\d+" "123 456" `shouldBe` ["123"]
+
     where
     issue :: Int -> Expectation -> Spec
-    issue n = it $ "https://github.com/sjshuck/pcre2/issues/" ++ show n
+    issue n = it $ "https://github.com/sjshuck/hs-pcre2/issues/" ++ show n
 
 onlyCausesOneCompilation :: (Option -> Text -> a) -> Expectation
 onlyCausesOneCompilation regexWithOpt = do
