diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
 *Megaparsec follows [SemVer](https://semver.org/).*
 
+## Megaparsec 9.8.0
+
+* Fixed the associativity of the `(<|>)` operator. [Issue
+  412](https://github.com/mrkkrp/megaparsec/issues/412).
+* Fixed the loss of precision in `decimal`, `binary`, `octal`, and
+  `hexadecimal` functions in `Text.Megaparsec.Byte.Lexer` and
+  `Text.Megaparsec.Char.Lexer` when they are used to parse floating point
+  numbers. [Issue 479](https://github.com/mrkkrp/megaparsec/issues/479).
+* Fixed handling of zero-width characters in error messages. To that end,
+  added `isZeroWidthChar` function in `Text.Megaparsec.Unicode`. [Issue
+  572](https://github.com/mrkkrp/megaparsec/issues/572).
+
 ## Megaparsec 9.7.1
 
 * Typo fixes and compatibility with `QuickCheck >= 2.17` for
diff --git a/Text/Megaparsec/Byte/Lexer.hs b/Text/Megaparsec/Byte/Lexer.hs
--- a/Text/Megaparsec/Byte/Lexer.hs
+++ b/Text/Megaparsec/Byte/Lexer.hs
@@ -41,7 +41,6 @@
 
 import Control.Applicative
 import Data.Functor (void)
-import Data.List (foldl')
 import Data.Proxy
 import Data.Scientific (Scientific)
 import qualified Data.Scientific as Sci
@@ -122,7 +121,7 @@
   m a
 decimal_ = mkNum <$> takeWhile1P (Just "digit") isDigit
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a w = a * 10 + fromIntegral (w - 48)
 {-# INLINE decimal_ #-}
 
@@ -145,7 +144,7 @@
     <$> takeWhile1P Nothing isBinDigit
     <?> "binary integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a w = a * 2 + fromIntegral (w - 48)
     isBinDigit w = w == 48 || w == 49
 {-# INLINEABLE binary #-}
@@ -170,7 +169,7 @@
     <$> takeWhile1P Nothing isOctDigit
     <?> "octal integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a w = a * 8 + fromIntegral (w - 48)
     isOctDigit w = w - 48 < 8
 {-# INLINEABLE octal #-}
@@ -195,7 +194,7 @@
     <$> takeWhile1P Nothing isHexDigit
     <?> "hexadecimal integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a w
       | w >= 48 && w <= 57 = a * 16 + fromIntegral (w - 48)
       | w >= 97 = a * 16 + fromIntegral (w - 87)
diff --git a/Text/Megaparsec/Char/Lexer.hs b/Text/Megaparsec/Char/Lexer.hs
--- a/Text/Megaparsec/Char/Lexer.hs
+++ b/Text/Megaparsec/Char/Lexer.hs
@@ -69,7 +69,6 @@
 import Control.Applicative
 import Control.Monad (void)
 import qualified Data.Char as Char
-import Data.List (foldl')
 import Data.List.NonEmpty (NonEmpty (..))
 import Data.Maybe (fromMaybe, isJust, listToMaybe)
 import Data.Proxy
@@ -372,7 +371,7 @@
   m a
 decimal_ = mkNum <$> takeWhile1P (Just "digit") Char.isDigit
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a c = a * 10 + fromIntegral (Char.digitToInt c)
 {-# INLINE decimal_ #-}
 
@@ -395,7 +394,7 @@
     <$> takeWhile1P Nothing isBinDigit
     <?> "binary integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a c = a * 2 + fromIntegral (Char.digitToInt c)
     isBinDigit x = x == '0' || x == '1'
 {-# INLINEABLE binary #-}
@@ -423,7 +422,7 @@
     <$> takeWhile1P Nothing Char.isOctDigit
     <?> "octal integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a c = a * 8 + fromIntegral (Char.digitToInt c)
 {-# INLINEABLE octal #-}
 
@@ -450,7 +449,7 @@
     <$> takeWhile1P Nothing Char.isHexDigit
     <?> "hexadecimal integer"
   where
-    mkNum = foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
+    mkNum = fromInteger . foldl' step 0 . chunkToTokens (Proxy :: Proxy s)
     step a c = a * 16 + fromIntegral (Char.digitToInt c)
 {-# INLINEABLE hexadecimal #-}
 
diff --git a/Text/Megaparsec/Internal.hs b/Text/Megaparsec/Internal.hs
--- a/Text/Megaparsec/Internal.hs
+++ b/Text/Megaparsec/Internal.hs
@@ -357,9 +357,32 @@
   let meerr err ms =
         let ncerr err' s' = cerr (err' <> err) (longestMatch ms s')
             neok x s' hs = eok x s' (toHints (stateOffset s') err <> hs)
-            neerr err' s' = eerr (err' <> err) (longestMatch ms s')
+            neerr err' s' =
+              let combinedErr = combineErrors (stateOffset s) err err'
+               in eerr combinedErr (longestMatch ms s')
          in unParser n s cok ncerr neok neerr
    in unParser m s cok cerr eok meerr
+  where
+    combineErrors altOffset e1 e2 = case (e1, e2) of
+      (TrivialError o1 u1 p1, TrivialError o2 u2 p2) ->
+        -- When merging alternative errors, if one is ahead due to try, we
+        -- bring both to the alternative position and union their expected
+        -- tokens.
+        if o1 > altOffset || o2 > altOffset
+          then
+            -- At least one error is ahead, normalize to alt position. Only
+            -- include expected tokens from errors at the alt position.
+            let p1' = if o1 == altOffset then p1 else E.empty
+                p2' = if o2 == altOffset then p2 else E.empty
+                -- Use the unexpected from the error at alt position, or the
+                -- furthest.
+                unexp = case (o1 `compare` altOffset, o2 `compare` altOffset) of
+                  (EQ, _) -> u1
+                  (_, EQ) -> u2
+                  _ -> if o1 >= o2 then u1 else u2
+             in TrivialError altOffset unexp (E.union p1' p2')
+          else e2 <> e1
+      _ -> e2 <> e1
 {-# INLINE pPlus #-}
 
 -- | From two states, return the one with the greater number of processed
diff --git a/Text/Megaparsec/Stream.hs b/Text/Megaparsec/Stream.hs
--- a/Text/Megaparsec/Stream.hs
+++ b/Text/Megaparsec/Stream.hs
@@ -40,7 +40,7 @@
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.ByteString.Lazy.Char8 as BL8
 import Data.Char (chr)
-import Data.Foldable (foldl', toList)
+import Data.Foldable (toList)
 import Data.Kind (Type)
 import Data.List.NonEmpty (NonEmpty (..))
 import qualified Data.List.NonEmpty as NE
@@ -568,8 +568,8 @@
   (Token s -> Char) ->
   -- | Newline token and tab token
   (Token s, Token s) ->
-  -- | Increment in column position for a token
-  (Token s -> Pos) ->
+  -- | Update column position for a token
+  (Token s -> Pos -> Pos) ->
   -- | Offset to reach
   Int ->
   -- | Initial 'PosState' to use
@@ -631,7 +631,7 @@
                     (g . (fromTok ch :))
               | otherwise ->
                   St
-                    (SourcePos n l (c <> columnIncrement ch))
+                    (SourcePos n l (columnIncrement ch c))
                     (g . (fromTok ch :))
 {-# INLINE reachOffset' #-}
 
@@ -645,9 +645,9 @@
   (forall b. (b -> Token s -> b) -> b -> Tokens s -> b) ->
   -- | Newline token and tab token
   (Token s, Token s) ->
+  -- | Update column position for a token
+  (Token s -> Pos -> Pos) ->
   -- | Offset to reach
-  -- | Increment in column position for a token
-  (Token s -> Pos) ->
   Int ->
   -- | Initial 'PosState' to use
   PosState s ->
@@ -680,7 +680,7 @@
               | ch == tabTok ->
                   SourcePos n l (mkPos $ c' + w - ((c' - 1) `rem` w))
               | otherwise ->
-                  SourcePos n l (c <> columnIncrement ch)
+                  SourcePos n l (columnIncrement ch c)
 {-# INLINE reachOffsetNoLine' #-}
 
 -- | Like 'BL.splitAt' but accepts the index as an 'Int'.
@@ -764,12 +764,15 @@
     go !i n xs = ' ' : go (i + 1) (n - 1) xs
     w = unPos w'
 
--- | Return increment in column position that corresponds to the given
--- 'Char'.
-charInc :: Char -> Pos
-charInc ch = if Unicode.isWideChar ch then pos1 <> pos1 else pos1
+-- | Return updated column position that corresponds to the given 'Char'.
+charInc :: Char -> Pos -> Pos
+charInc ch c
+  | Unicode.isZeroWidthChar ch = c
+  | Unicode.isWideChar ch = c <> pos1 <> pos1
+  | otherwise = c <> pos1
 
--- | Return increment in column position that corresponds to the given
--- 'Word8'.
-byteInc :: Word8 -> Pos
-byteInc _ = pos1
+-- | Return updated column position that corresponds to the given 'Word8'.
+byteInc :: Word8 -> Pos -> Pos
+byteInc w c
+  | w < 0x20 || (w >= 0x7f && w < 0xa0) = c -- C0 and C1 control chars
+  | otherwise = c <> pos1
diff --git a/Text/Megaparsec/Unicode.hs b/Text/Megaparsec/Unicode.hs
--- a/Text/Megaparsec/Unicode.hs
+++ b/Text/Megaparsec/Unicode.hs
@@ -16,6 +16,7 @@
   ( stringLength,
     charLength,
     isWideChar,
+    isZeroWidthChar,
   )
 where
 
@@ -33,7 +34,10 @@
 --
 -- @since 9.7.0
 charLength :: Char -> Int
-charLength ch = if isWideChar ch then 2 else 1
+charLength ch
+  | isZeroWidthChar ch = 0
+  | isWideChar ch = 2
+  | otherwise = 1
 
 -- | Determine whether the given 'Char' is “wide”, that is, whether it spans
 -- 2 columns instead of one.
@@ -52,6 +56,24 @@
         (a, b) = wideCharRanges ! mid
     n = ord c
 
+-- | Determine whether the given 'Char' is "zero-width", that is, whether it
+-- has no visible representation and does not advance the cursor position.
+-- This includes control characters and certain Unicode zero-width characters.
+--
+-- @since 9.8.0
+isZeroWidthChar :: Char -> Bool
+isZeroWidthChar c = go (bounds zeroWidthCharRanges)
+  where
+    go (lo, hi)
+      | hi < lo = False
+      | a <= n && n <= b = True
+      | n < a = go (lo, pred mid)
+      | otherwise = go (succ mid, hi)
+      where
+        mid = (lo + hi) `div` 2
+        (a, b) = zeroWidthCharRanges ! mid
+    n = ord c
+
 -- | Wide character ranges.
 wideCharRanges :: Array Int (Int, Int)
 wideCharRanges =
@@ -178,3 +200,24 @@
       (0x02f800, 0x02fa1d)
     ]
 {-# NOINLINE wideCharRanges #-}
+
+-- | Zero-width character ranges.
+zeroWidthCharRanges :: Array Int (Int, Int)
+zeroWidthCharRanges =
+  listArray
+    (0, 12)
+    [ (0x0000, 0x001f), -- C0 control characters
+      (0x007f, 0x009f), -- DEL and C1 control characters
+      (0x00ad, 0x00ad), -- Soft Hyphen
+      (0x0300, 0x036f), -- Combining Diacritical Marks
+      (0x0483, 0x0489), -- Combining Cyrillic
+      (0x0591, 0x05bd), -- Hebrew combining marks
+      (0x05bf, 0x05bf), -- Hebrew point
+      (0x05c1, 0x05c2), -- Hebrew points
+      (0x05c4, 0x05c5), -- Hebrew marks
+      (0x05c7, 0x05c7), -- Hebrew point
+      (0x0610, 0x061a), -- Arabic combining marks
+      (0x200b, 0x200f), -- Zero width chars and directional marks
+      (0x202a, 0x202e) -- Directional formatting
+    ]
+{-# NOINLINE zeroWidthCharRanges #-}
diff --git a/megaparsec.cabal b/megaparsec.cabal
--- a/megaparsec.cabal
+++ b/megaparsec.cabal
@@ -1,6 +1,6 @@
 cabal-version:   2.4
 name:            megaparsec
-version:         9.7.1
+version:         9.8.0
 license:         BSD-2-Clause
 license-file:    LICENSE.md
 maintainer:      Mark Karpov <markkarpov92@gmail.com>
@@ -72,7 +72,7 @@
     if flag(dev)
         ghc-options:
             -Wall -Werror -Wredundant-constraints -Wpartial-fields
-            -Wunused-packages -Wno-unused-imports
+            -Wunused-packages
 
     else
         ghc-options: -O2 -Wall
