diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,9 @@
 # Changelog and Acknowledgements
 
+## 1.1.1
+* Fixed [#12](https://github.com/sjshuck/pcre2/4), where some functions returned
+  too many match results.
+
 ## 1.1.0
 * Added global matching.
     * New functions `matchAll`, `matchAllOpt`, `capturesAll`, `capturesAllOpt`.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@
   Both are first-class.
 * Vast presentation of PCRE2 functionality.  We can even register Haskell
   callbacks to run during matching!
-* No dependencies that aren't distributed with GHC.
+* Few dependencies.
 * Bundled, statically-linked UTF-16 build of ~~up-to-date~~ PCRE2 (version
   10.35), with a complete, exposed Haskell binding.
 
diff --git a/pcre2.cabal b/pcre2.cabal
--- a/pcre2.cabal
+++ b/pcre2.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 0a7a573e61659cfbca9092c36ae2549d63b0c6745b4bc3953f29dfc97aebd1b6
+-- hash: d8aba62e213c9b15ec9218a159e7922eb4ebb298385cd3ff0e1acd1f15fd6506
 
 name:           pcre2
-version:        1.1.0
+version:        1.1.1
 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/hs/Text/Regex/Pcre2/Foreign.hs b/src/hs/Text/Regex/Pcre2/Foreign.hs
--- a/src/hs/Text/Regex/Pcre2/Foreign.hs
+++ b/src/hs/Text/Regex/Pcre2/Foreign.hs
@@ -5,7 +5,7 @@
 --
 -- Items here are named identically to their C counterparts.  Therefore,
 -- documentation will be sparse;
--- [the official PCRE2 API docs](https://pcre.org/current/doc/html/pcre2api.html#SEC36)
+-- [the official PCRE2 API docs](https://pcre.org/current/doc/html/pcre2api.html)
 -- should suffice.
 module Text.Regex.Pcre2.Foreign where
 
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
@@ -150,10 +150,11 @@
 -- * Assembling inputs into @Matcher@s and @Subber@s
 
 -- | A matching function where all inputs and auxilliary data structures have
--- been \"compiled\".  It takes a subject and produces a list of match data
--- objects, representing a global match, to be passed to further validation and
--- inspection.
-type Matcher = Text -> IO [MatchData]
+-- been \"compiled\".  It takes a subject and a whitelist of capture indexes
+-- to extract, and produces lists of capture offsets representing a global
+-- match.  The whitelist is @[]@ rather than @NonEmpty@ to facilitate writing
+-- literals.
+type Matcher = Text -> [Int] -> IO [NonEmpty SliceRange]
 
 -- | A substitution function.  It takes a subject and produces the number of
 -- substitutions performed (0 or 1, or more in the presence of `SubGlobal`)
@@ -658,12 +659,12 @@
     return $ MatchEnv {..}
 
 -- | Helper for @assemble*@ functions.  Basically, extract all options and help
--- produce a function that takes a `Text` subject and does something.
+-- produce a function that takes a `Text` subject.
 assembleSubjFun
-    :: (MatchEnv -> Text -> IO a)
+    :: (MatchEnv -> Text -> a)
     -> Option
     -> Text
-    -> IO (Text -> IO a)
+    -> IO (Text -> a)
 assembleSubjFun mkSubjFun option patt =
     runStateT extractAll (applyOption option) <&> \case
         (subjFun, []) -> subjFun
@@ -684,27 +685,29 @@
     -> IO Matcher
 assembleMatcher = assembleSubjFun $ \matchEnv@(MatchEnv {..}) ->
     let matchTempEnvWithSubj = mkMatchTempEnv matchEnv
-    in \subject ->
+    in \subject whitelist ->
         withForeignPtr matchEnvCode $ \codePtr ->
         Text.useAsPtr subject $ \subjPtr subjCUs -> do
             MatchTempEnv {..} <- matchTempEnvWithSubj subject
 
-            -- Loop over the subject, emitting MatchData's until stopping.
-            execWriterT $ runMaybeT $ fix1 0 $ \continue curOff -> do
-                -- TODO Can one of these be reused for a whole global match?
-                matchData <- liftIO $ mkForeignPtr pcre2_match_data_free $
-                    pcre2_match_data_create_from_pattern codePtr nullPtr
+            -- These need to happen one after the other.  We need the
+            -- ForeignPtr to touch later, to allow cleanup after lazy matching.
+            matchDataPtr <- pcre2_match_data_create_from_pattern codePtr nullPtr
+            matchData <-
+                Conc.newForeignPtr <*> pcre2_match_data_free $ matchDataPtr
 
+            -- Loop over the subject, emitting slice lists until stopping.
+            lists <- execWriterT $ runMaybeT $ fix1 0 $ \continue curOff -> do
                 result <- liftIO $
                     withForeignOrNullPtr matchTempEnvCtx $ \ctxPtr ->
-                    withForeignPtr matchData $ \matchDataPtr -> pcre2_match
-                        codePtr
-                        (castCUs subjPtr)
-                        (fromIntegral subjCUs)
-                        curOff
-                        matchEnvOpts
-                        matchDataPtr
-                        ctxPtr
+                        pcre2_match
+                            codePtr
+                            (castCUs subjPtr)
+                            (fromIntegral subjCUs)
+                            curOff
+                            matchEnvOpts
+                            matchDataPtr
+                            ctxPtr
 
                 -- Handle no match and errors
                 when (result == pcre2_ERROR_NOMATCH) stop
@@ -712,22 +715,30 @@
                     liftIO $ maybeRethrow matchTempEnvRef
                 liftIO $ check (> 0) result
 
-                emit matchData
+                slices <- liftIO $ unsafeInterleaveIO $
+                    getOvecEntriesAt whitelist matchDataPtr
 
+                emit slices
+
                 lazy $ do
                     -- Determine next starting offset
-                    nextOff <- liftIO $
-                        withForeignPtr matchData $ \matchDataPtr -> do
-                            ovecPtr <- pcre2_get_ovector_pointer matchDataPtr
-                            curOffEnd <- peekElemOff ovecPtr 1
-                            -- Prevent infinite loop upon empty match
-                            return $ max curOffEnd (curOff + 1)
+                    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) stop
 
-                    continue nextOff
+                    -- Force slices out of the current match_data state before
+                    -- clobbering it with the next match
+                    slices `seq` continue nextOff
 
+            touchForeignPtr matchData
+
+            return lists
+
     where
     stop = MaybeT $ return Nothing
     emit x = tell [x]
@@ -1028,17 +1039,15 @@
 -- Internal only!  Users should not (have to) know about `Matcher`.
 _capturesInternal :: Matcher -> [Int] -> Traversal' Text (NonEmpty Text)
 _capturesInternal matcher whitelist f subject = traverse f css <&> \css' ->
-    -- Swag foldl-as-foldr to create only as many segments as we need to
-    -- stitch back together and no more.
+    -- Swag foldl-as-foldr to create only as many segments as we need to stitch
+    -- back together and no more.
     let triples = zip3 (flatten ovecEntries) (flatten css) (flatten css')
     in Text.concat $ foldr mkSegments termSegments triples 0
 
     where
 
     flatten = concatMap NE.toList
-    ovecEntries = do
-        matchData <- unsafePerformIO $ matcher subject
-        return $ unsafePerformIO $ getOvecEntriesAt whitelist matchData
+    ovecEntries = unsafePerformIO $ matcher subject whitelist
     css = do
         ovecEntry <- ovecEntries
         return $ unsafePerformIO $ mapM (evaluate . slice subject) ovecEntry
@@ -1061,12 +1070,12 @@
 
 -- | Strictly read specifically indexed captures' offsets from match results.
 -- Truncate indexes when they go out of bounds, to support infinite lists.
-getOvecEntriesAt :: [Int] -> MatchData -> IO (NonEmpty SliceRange)
-getOvecEntriesAt ns matchData = withForeignPtr matchData $ \matchDataPtr -> do
+getOvecEntriesAt :: [Int] -> Ptr Pcre2_match_data -> IO (NonEmpty SliceRange)
+getOvecEntriesAt whitelist matchDataPtr = do
     count <- fromIntegral <$> pcre2_get_ovector_count matchDataPtr
 
-    case NE.nonEmpty $ takeWhile (< count) ns of
-        Nothing -> error "BUG! Tried to get empty list of captures"
+    case NE.nonEmpty $ takeWhile (< count) whitelist of
+        Nothing -> error "BUG! Empty whitelist"
         Just ns -> do
             ovecPtr <- pcre2_get_ovector_pointer matchDataPtr
 
@@ -1089,7 +1098,8 @@
 
 -- | @capturesOpt mempty = captures@
 capturesOpt :: Option -> Text -> Text -> [Text]
-capturesOpt option patt = view $ _capturesOpt option patt . to NE.toList
+capturesOpt option patt =
+    maybe [] NE.toList . preview (_capturesOpt 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
@@ -1108,9 +1118,8 @@
 capturesAOpt :: (Alternative f) => Option -> Text -> Text -> f (NonEmpty Text)
 capturesAOpt option patt = toAlternativeOf $ _capturesOpt option patt
 
--- | Match a pattern to a subject and lazily return zero or more non-empty lists
--- of captures corresponding to every non-overlapping place in the subject the
--- pattern matched.
+-- | Match a pattern to a subject and lazily a list of all non-overlapping
+-- portions, with all capture groups, that matched.
 --
 -- @since 1.1.0
 capturesAll :: Text -> Text -> [NonEmpty Text]
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
@@ -126,7 +126,7 @@
                 (nums, names) = unzip numberedNames
                 e = [e|
                     let _cs = _capturesInternal $(matcherQ s) $(liftData nums)
-                    in view $ _cs . to NE.toList |]
+                    in maybe [] NE.toList . preview _cs |]
                 p = listP $ map (varP . mkName . Text.unpack) names,
 
     quoteType = const $ fail "regex: cannot produce a type",
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -215,6 +215,13 @@
         f "a" `shouldReturn` ""
         f "b" `shouldReturn` "b"
 
+    issue 12 $ do
+        captures ".a." "foo bar baz" `shouldBe` ["bar"]
+        case "foo bar baz" of
+            [regex|.a(?<x>.)|] -> return ()
+            _                  ->
+                expectationFailure "quasi-quoted pattern didn't match"
+
     where
     issue :: Int -> Expectation -> Spec
     issue n = it $ "https://github.com/sjshuck/issues/" ++ show n
