diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Johannes Hilden (c) 2017
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of copyright holder nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+# fuzzyset-haskell [![Build Status](https://img.shields.io/travis/laserpants/fuzzyset-haskell/master.svg?style=flat)](https://travis-ci.org/laserpants/fuzzyset-haskell)
+
+A fuzzy string set data structure for approximate string matching. This implementation is based on the Python and JavaScript libraries with the same name:
+
+* [JavaScript version](https://github.com/Glench/fuzzyset.js)
+* [Python version](https://github.com/axiak/fuzzyset)
+
+## How to use
+
+Make sure the `OverloadedStrings` pragma is enabled. Then there are just three steps:
+
+1. Create a set using one of `defaultSet`, `mkSet`, or `fromList`.
+2. To add entries, use `add`, `addToSet`, or `addMany`.
+3. Then query the set with `get`, `getOne`, or `getWithMinScore`.
+
+```
+>>> defaultSet `add` "Jurassic Park" `add` "Terminator" `add` "The Matrix" `getOne` "percolator"
+Just "Terminator"
+```
+
+```
+>>> defaultSet `add` "Shaggy Rogers" `add` "Fred Jones" `add` "Daphne Blake" `add` "Velma Dinkley" `get` "Shaggy Jones"
+[(0.7692307692307693,"Shaggy Rogers"),(0.5,"Fred Jones")]
+```
+
+There are also a few functions to inspect the set.
+
+### More examples
+
+```
+{-# LANGUAGE OverloadedStrings #-}
+module Main where
+
+import Data.FuzzySet
+
+states = [ "Alabama"        , "Alaska"         , "American Samoa"            , "Arizona"       , "Arkansas"
+         , "California"     , "Colorado"       , "Connecticut"               , "Delaware"      , "District of Columbia"
+         , "Florida"        , "Georgia"        , "Guam"                      , "Hawaii"        , "Idaho"
+         , "Illinois"       , "Indiana"        , "Iowa"                      , "Kansas"        , "Kentucky"
+         , "Louisiana"      , "Maine"          , "Maryland"                  , "Massachusetts" , "Michigan"
+         , "Minnesota"      , "Mississippi"    , "Missouri"                  , "Montana"       , "Nebraska"
+         , "Nevada"         , "New Hampshire"  , "New Jersey"                , "New Mexico"    , "New York"
+         , "North Carolina" , "North Dakota"   , "Northern Marianas Islands" , "Ohio"          , "Oklahoma"
+         , "Oregon"         , "Pennsylvania"   , "Puerto Rico"               , "Rhode Island"  , "South Carolina"
+         , "South Dakota"   , "Tennessee"      , "Texas"                     , "Utah"          , "Vermont"
+         , "Virginia"       , "Virgin Islands" , "Washington"                , "West Virginia" , "Wisconsin"
+         , "Wyoming" ]
+
+statesSet = fromList states
+
+main = mapM_ print (get statesSet "Burger Islands")
+```
+
+The output of this program is:
+
+```
+(0.7142857142857143,"Virgin Islands")
+(0.5714285714285714,"Rhode Island")
+(0.44,"Northern Marianas Islands")
+(0.35714285714285715,"Maryland")
+```
+
+Using the definition of `statesSet` from previous example:
+
+```
+>>> get statesSet "Why-oh-me-ing"
+[(0.5384615384615384,"Wyoming")]
+
+>>> get statesSet "Connect a cat"
+[(0.6923076923076923,"Connecticut")]
+
+>>> get statesSet "Transylvania"
+[(0.75,"Pennsylvania"),(0.3333333333333333,"California"),(0.3333333333333333,"Arkansas"),(0.3333333333333333,"Kansas")]
+
+>>> get statesSet "CanOfSauce"
+[(0.4,"Kansas")]
+
+>>> get statesSet "Alaska"
+[(1.0,"Alaska")]
+
+>>> get statesSet "Alaskanbraskansas"
+[(0.47058823529411764,"Arkansas"),(0.35294117647058826,"Kansas"),(0.35294117647058826,"Alaska"),(0.35294117647058826,"Alabama"),(0.35294117647058826,"Nebraska")]
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import Data.FuzzySet
+
+main :: IO ()
+main = return ()
diff --git a/fuzzyset.cabal b/fuzzyset.cabal
new file mode 100644
--- /dev/null
+++ b/fuzzyset.cabal
@@ -0,0 +1,58 @@
+name:                fuzzyset
+version:             0.1.0.0
+synopsis:            Fuzzy set for approximate string matching
+description:         This library is based on the Python and JavaScript libraries with the same name.
+homepage:            https://github.com/laserpants/fuzzyset-haskell
+license:             BSD3
+license-file:        LICENSE
+author:              Johannes Hildén
+maintainer:          hildenjohannes@gmail.com
+copyright:           2017 Johannes Hildén
+category:            Web
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     Data.FuzzySet
+                     , Data.FuzzySet.Internal
+                     , Data.FuzzySet.Lens
+                     , Data.FuzzySet.Types
+                     , Data.FuzzySet.Util
+  build-depends:       base >= 4.7 && < 5
+                     , base-unicode-symbols ==0.2.2.4
+                     , data-default ==0.7.1.1
+                     , lens ==4.15.4
+                     , text ==1.2.2.2
+                     , text-metrics ==0.3.0
+                     , unordered-containers ==0.2.8.0
+                     , vector ==0.12.0.1
+  default-language:    Haskell2010
+
+executable fuzzyset-exe
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base
+                     , fuzzyset
+  default-language:    Haskell2010
+
+test-suite fuzzyset-test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  build-depends:       base
+                     , base-unicode-symbols ==0.2.2.4
+                     , hspec ==2.4.4
+                     , fuzzyset
+                     , lens ==4.15.4
+                     , text ==1.2.2.2
+                     , ieee754 ==0.8.0
+                     , unordered-containers ==0.2.8.0
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/laserpants/fuzzyset-haskell
diff --git a/src/Data/FuzzySet.hs b/src/Data/FuzzySet.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FuzzySet.hs
@@ -0,0 +1,262 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE UnicodeSyntax   #-}
+
+-- |
+--
+-- Module      : Data.FuzzySet
+-- Copyright   : (c) 2017 Johannes Hildén
+-- License     : BSD3
+-- Maintainer  : hildenjohannes@gmail.com
+-- Stability   : experimental
+-- Portability : GHC
+--
+-- A fuzzy string set data structure for approximate string matching. This 
+-- implementation is based on the Python and JavaScript libraries with the same
+-- name: 
+--
+--   * [JavaScript version](http://glench.github.io/fuzzyset.js/)
+--   * [Python version](https://github.com/axiak/fuzzyset)
+
+module Data.FuzzySet
+  ( 
+  -- * How to use
+  -- $howto
+
+  -- * Types
+    FuzzySet
+  , Size
+
+  -- * API
+
+  -- ** Initializing
+  , mkSet
+  , defaultSet
+  , fromList
+
+  -- ** Adding
+  , add
+  , addToSet
+  , addMany
+
+  -- ** Retrieving
+  , get
+  , getWithMinScore
+  , getOne
+
+  -- ** Inspecting
+  , size
+  , isEmpty
+  , values
+  ) where
+
+import Data.Foldable.Unicode
+import Data.FuzzySet.Internal
+import Data.FuzzySet.Lens
+import Data.FuzzySet.Types
+import Data.FuzzySet.Util
+import Data.HashMap.Strict             ( HashMap, elems, insert, unionWith )
+import Data.Maybe                      ( fromMaybe )
+import Data.List                       ( find )
+import Data.Text                       ( Text )
+import Prelude.Unicode                 hiding ( (∈) )
+
+import qualified Data.Text             as Text
+import qualified Data.HashMap.Strict   as HashMap
+import qualified Data.Vector           as Vector
+
+-- $howto
+--
+-- Make sure the @OverloadedStrings@ pragma is enabled. Then there are just 
+-- three steps:
+--
+--   1. Create a set using one of 'defaultSet', 'mkSet', or 'fromList'.
+--   2. To add entries, use 'add', 'addToSet', or 'addMany'.
+--   3. Then query the set with 'get', 'getOne', or 'getWithMinScore'.
+--
+-- >>> defaultSet `add` "Jurassic Park" `add` "Terminator" `add` "The Matrix" `getOne` "percolator"
+-- Just "Terminator"
+--
+-- >>> defaultSet `add` "Shaggy Rogers" `add` "Fred Jones" `add` "Daphne Blake" `add` "Velma Dinkley" `get` "Shaggy Jones"
+-- [(0.7692307692307693,"Shaggy Rogers"),(0.5,"Fred Jones")]
+--
+-- There are also a few functions to [inspect](#g:7) the set.
+--
+-- == More examples
+--
+-- > {-# LANGUAGE OverloadedStrings #-}
+-- > module Main where
+-- > 
+-- > import Data.FuzzySet
+-- > 
+-- > states = [ "Alabama"        , "Alaska"         , "American Samoa"            , "Arizona"       , "Arkansas"
+-- >          , "California"     , "Colorado"       , "Connecticut"               , "Delaware"      , "District of Columbia"
+-- >          , "Florida"        , "Georgia"        , "Guam"                      , "Hawaii"        , "Idaho"
+-- >          , "Illinois"       , "Indiana"        , "Iowa"                      , "Kansas"        , "Kentucky"
+-- >          , "Louisiana"      , "Maine"          , "Maryland"                  , "Massachusetts" , "Michigan"
+-- >          , "Minnesota"      , "Mississippi"    , "Missouri"                  , "Montana"       , "Nebraska"
+-- >          , "Nevada"         , "New Hampshire"  , "New Jersey"                , "New Mexico"    , "New York"
+-- >          , "North Carolina" , "North Dakota"   , "Northern Marianas Islands" , "Ohio"          , "Oklahoma"
+-- >          , "Oregon"         , "Pennsylvania"   , "Puerto Rico"               , "Rhode Island"  , "South Carolina"
+-- >          , "South Dakota"   , "Tennessee"      , "Texas"                     , "Utah"          , "Vermont"
+-- >          , "Virginia"       , "Virgin Islands" , "Washington"                , "West Virginia" , "Wisconsin"
+-- >          , "Wyoming" ]
+-- >
+-- > statesSet = fromList states
+-- > 
+-- > main = mapM_ print (get statesSet "Burger Islands")
+--
+-- The output of this program is:
+--
+-- > (0.7142857142857143,"Virgin Islands")
+-- > (0.5714285714285714,"Rhode Island")
+-- > (0.44,"Northern Marianas Islands")
+-- > (0.35714285714285715,"Maryland")
+--
+--  Using the definition of @statesSet@ from previous example:
+--
+-- > >>> get statesSet "Why-oh-me-ing"
+-- > [(0.5384615384615384,"Wyoming")]
+--
+-- > >>> get statesSet "Connect a cat"
+-- > [(0.6923076923076923,"Connecticut")]
+-- 
+-- > >>> get statesSet "Transylvania"
+-- > [(0.75,"Pennsylvania"),(0.3333333333333333,"California"),(0.3333333333333333,"Arkansas"),(0.3333333333333333,"Kansas")]
+--
+-- > >>> get statesSet "CanOfSauce"
+-- > [(0.4,"Kansas")]
+--
+-- > >>> get statesSet "Alaska"
+-- > [(1.0,"Alaska")]
+--
+-- > >>> get statesSet "Alaskanbraskansas"
+-- > [(0.47058823529411764,"Arkansas"),(0.35294117647058826,"Kansas"),(0.35294117647058826,"Alaska"),(0.35294117647058826,"Alabama"),(0.35294117647058826,"Nebraska")]
+
+-- | Initialize a 'FuzzySet'.
+mkSet ∷ Size 
+      -- ^ The lower bound of gram sizes to use (inclusive)
+      → Size 
+      -- ^ The upper bound of gram sizes to use (inclusive)
+      → Bool 
+      -- ^ Whether to use [Levenshtein distance](https://people.cs.pitt.edu/~kirk/cs1501/Pruhs/Spring2006/assignments/editdistance/Levenshtein%20Distance.htm) 
+      --   to determine the score
+      → FuzzySet
+      -- ^ An empty fuzzy string set 
+mkSet lower upper levenshtein = FuzzySet lower upper levenshtein ε ε ε
+
+-- | Try to match the given string against the entries in the set, and return
+--   a list of all results with a score greater than or equal to the specified 
+--   minimum score (i.e., the first argument). The results are ordered by
+--   similarity score, with the closest match first.
+getWithMinScore ∷ Double
+                -- ^ A minimum score
+                → FuzzySet
+                -- ^ The fuzzy string set to compare the string against
+                → Text
+                -- ^ The lookup query
+                → [(Double, Text)]
+                -- ^ A list of results (score and matched value pairs)
+getWithMinScore minScore FuzzySet{..} val =
+    case HashMap.lookup key exactSet of
+      Just v  → [(1, v)]
+      Nothing → fromMaybe [] $ find (not ∘ null) (getMatch ctx <$> sizes)
+  where
+    ctx = GetContext key minScore FuzzySet{..}
+    key = Text.toLower val
+    sizes = reverse [gramSizeLower .. gramSizeUpper]
+
+-- | Try to match the given string against the entries in the set, using a
+--   minimum score of 0.33. Return a list of results ordered by similarity 
+--   score, with the closest match first.
+get ∷ FuzzySet
+    -- ^ The fuzzy string set to compare the string against
+    → Text
+    -- ^ The lookup query
+    → [(Double, Text)]
+    -- ^ A list of results (score and matched value pairs)
+get = getWithMinScore 0.33
+
+-- | Try to match the given string against the entries in the set, and return
+--   the closest match, if one is found.
+getOne ∷ FuzzySet
+       -- ^ The fuzzy string set to compare the string against
+       → Text
+       -- ^ The lookup query
+       → Maybe Text
+       -- ^ 'Just' the result, if one was found, otherwise 'Nothing'
+getOne set val = 
+    case get set val of
+      [] → Nothing
+      xs → Just (snd (head xs))
+
+-- | Add an entry to the set, or do nothing if a key identical to the provided
+--   value already exists in the set.
+add ∷ FuzzySet
+    -- ^ Fuzzy string set to add the entry to
+    → Text
+    -- ^ The new entry
+    → FuzzySet
+    -- ^ The updated set
+add set = fst ∘ addToSet set
+
+-- | Add an entry to the set and return a pair with the new set, and a boolean
+--   to indicate if a new entry was inserted, or not.
+addToSet ∷ FuzzySet
+         -- ^ Fuzzy string set to add the entry to
+         → Text
+         -- ^ The new entry
+         → (FuzzySet, Bool)
+         -- ^ The updated set and a boolean, which will be 'True' if, and only 
+         --   if, the value was not already in the set 
+addToSet FuzzySet{..} val
+    | key ∈ exactSet = (FuzzySet{..}, False)
+    | otherwise =
+      let sizes = [gramSizeLower .. gramSizeUpper]
+       in (foldr ξ FuzzySet{..} sizes &_exactSet %~ insert key val, True)
+  where
+    key = Text.toLower val
+    ξ size fs =
+      let dict' = flip (:) [] ∘ GramInfo index <$> gramMap (normalized val) size
+          item  = FuzzySetItem (gramMap key size & elems & norm) key
+          index = fs ^._items ^? ix size ^._Just & Vector.length
+       in over _matchDict (\dict → unionWith (⧺) dict dict')
+        $ over (_items.at size) (Just ∘ (`Vector.snoc` item)
+                                      ∘ fromMaybe Vector.empty) fs
+
+-- | Add a list of entries to the set, in one go. 
+--
+-- > addMany = foldr (flip add)
+addMany ∷ FuzzySet
+        -- ^ Fuzzy string set to add the entries to
+        → [Text]
+        -- ^ A list of new entries
+        → FuzzySet
+        -- ^ A new fuzzy string set
+addMany = foldr (flip add)
+
+-- | Create a fuzzy string set with entries from the given list.
+--
+-- @fromList = addMany defaultSet@
+fromList ∷ [Text] → FuzzySet
+fromList = addMany defaultSet 
+
+-- | Return the number of entries in the set.
+--
+-- >>> size (defaultSet `add` "map" `add` "cap")
+-- 2
+size ∷ FuzzySet → Int
+size = HashMap.size ∘ exactSet
+
+-- | Return a boolean indicating whether the provided set is empty.
+--
+-- >>> isEmpty (fromList [])
+-- True
+isEmpty ∷ FuzzySet → Bool
+isEmpty = HashMap.null ∘ exactSet
+
+-- | Return the elements of the set.
+--
+-- >>> values (fromList ["bass", "craze", "space", "lace", "daze", "haze", "ace", "maze"])
+-- ["space","daze","bass","maze","ace","craze","lace","haze"]
+values ∷ FuzzySet → [Text]
+values = elems ∘ exactSet
diff --git a/src/Data/FuzzySet/Internal.hs b/src/Data/FuzzySet/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FuzzySet/Internal.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE UnicodeSyntax   #-}
+module Data.FuzzySet.Internal where
+
+import Data.Function                   ( on )
+import Data.FuzzySet.Lens
+import Data.FuzzySet.Types
+import Data.FuzzySet.Util
+import Data.HashMap.Strict             ( HashMap, alter, empty, elems, foldrWithKey )
+import Data.Maybe                      ( fromMaybe )
+import Data.List                       ( sortBy )
+import Data.Text                       ( Text )
+import Prelude.Unicode
+
+import qualified Data.HashMap.Strict   as HashMap
+import qualified Data.Text             as Text
+import qualified Data.Vector           as Vector
+
+getMatch ∷ GetContext → Size → [(Double, Text)]
+getMatch GetContext{..} size = match <$$> filtered
+  where
+    match α = set ^._exactSet.ix α
+    filtered = filter ((<) minScore ∘ fst) sorted
+    μ p = p & _1.~ distance (p ^._2) key
+    sorted = sortBy (flip compare `on` fst) $
+        let rs = results GetContext{..} size
+         in if set ^._useLevenshtein
+                then take 50 (μ <$> rs)
+                else rs
+
+results ∷ GetContext → Size → [(Double, Text)]
+results GetContext{..} size = ζ <$> HashMap.toList (matches set grams)
+  where
+    grams  = gramMap key size
+    normal = norm (elems grams)
+    ζ (index, score) =
+      let FuzzySetItem{..} = Vector.unsafeIndex (set ^._items.ix size) index
+       in (fromIntegral score / (normal × vectorMagnitude), normalizedEntry)
+
+matches ∷ FuzzySet → HashMap Text Int → HashMap Int Int
+matches set = foldrWithKey ζ empty
+  where
+    ζ gram occ m = foldr (\GramInfo{..} →
+        alter (pure ∘ (+) (occ × gramCount) ∘ fromMaybe 0) itemIndex)
+          m (set ^._matchDict.ix gram)
+
+-- | Normalize the input string, call 'grams' on the normalized input, and then
+--   translate the result to a 'HashMap' with the /n/-grams as keys and 'Int'
+--   values corresponding to the number of occurences of the key in the
+--   generated gram list.
+--
+-- >>> gramMap "xxxx" 2
+-- fromList [("-x",1), ("xx",3), ("x-",1)]
+--
+-- >>> Data.HashMap.Strict.lookup "nts" (gramMap "intrent'srestaurantsomeoftrent'saunt'santswantsamtorentsomepants" 3)
+-- Just 8
+gramMap ∷ Text
+        -- ^ An input string
+        → Size
+        -- ^ The gram size /n/, which must be at least /2/
+        → HashMap Text Int
+        -- ^ A mapping from /n/-gram keys to the number of occurrences of the
+        --   key in the list returned by 'grams' (i.e., the list of all
+        --   /n/-length substrings of the input enclosed in hyphens).
+gramMap val size = foldr ζ ε (grams val size)
+  where
+    ζ = alter (pure ∘ succ ∘ fromMaybe 0)
+
+-- | Break apart the normalized input string into a list of /n/-grams. For
+--   instance, the string "Destroido Corp." is first normalized into the
+--   form "destroido corp", and then enclosed in hyphens, so that it becomes
+--   "-destroido corp-". The /3/-grams generated from this normalized string are
+--
+--   > "-de", "des", "est", "str", "tro", "roi", "oid", "ido", "do ", "o c", " co", "cor", "orp", "rp-"
+--
+--   Given a normalized string of length /s/, we take all substrings of length
+--   /n/, letting the offset range from \(0 \text{ to } s + 2 − n\). The number
+--   of /n/-grams for a normalized string of length /s/ is thus
+--   \(s + 2 − n + 1 = s − n + 3\), where \(0 < n < s − 2\).
+grams ∷ Text   -- ^ An input string
+      → Size   -- ^ The variable /n/, which must be at least /2/
+      → [Text] -- ^ A /k/-length list of grams of size /n/,
+               --   with \(k = s − n + 3\)
+grams val size
+    | size < 2  = error "gram size must be >= 2"
+    | otherwise = ($ str) ∘ substr size <$> [0 .. Text.length str − size]
+  where
+    str = normalized val `enclosedIn` '-'
diff --git a/src/Data/FuzzySet/Lens.hs b/src/Data/FuzzySet/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FuzzySet/Lens.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE TemplateHaskell #-}
+module Data.FuzzySet.Lens
+  ( module Control.Lens
+  , _items
+  , _matchDict
+  , _exactSet
+  , _useLevenshtein
+  , _gramSizeLower
+  , _gramSizeUpper
+  , _vectorMagnitude
+  , _normalizedEntry
+  ) where
+
+import Control.Lens
+import Data.FuzzySet.Types
+
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Vector         as Vector
+
+makeLensesFor
+  [ ("items"           , "_items")
+  , ("matchDict"       , "_matchDict")
+  , ("exactSet"        , "_exactSet")
+  , ("useLevenshtein"  , "_useLevenshtein")
+  , ("gramSizeLower"   , "_gramSizeLower")
+  , ("gramSizeUpper"   , "_gramSizeUpper")
+  ] ''FuzzySet
+
+makeLensesFor
+  [ ("vectorMagnitude" , "_vectorMagnitude")
+  , ("normalizedEntry" , "_normalizedEntry")
+  ] ''FuzzySetItem
diff --git a/src/Data/FuzzySet/Types.hs b/src/Data/FuzzySet/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FuzzySet/Types.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE UnicodeSyntax #-}
+module Data.FuzzySet.Types where
+
+import Data.Default
+import Data.Text           ( Text )
+import Data.HashMap.Strict ( HashMap )
+import Data.Vector         ( Vector )
+
+data FuzzySetItem = FuzzySetItem
+  { vectorMagnitude ∷ !Double
+  , normalizedEntry ∷ !Text
+  } deriving (Eq, Show)
+
+data GramInfo = GramInfo
+  { itemIndex ∷ !Int
+  , gramCount ∷ !Int
+  } deriving (Eq, Show)
+
+-- | Type alias for representing gram sizes.
+type Size      = Int
+type ExactSet  = HashMap Text Text
+type MatchDict = HashMap Text [GramInfo]
+type ItemMap   = HashMap Size (Vector FuzzySetItem)
+
+-- | Opaque fuzzy string set data type. Use 'Data.FuzzySet.defaultSet', 
+--   'Data.FuzzySet.mkSet', or 'Data.FuzzySet.fromList' to create 'FuzzySet's.
+data FuzzySet = FuzzySet
+  { gramSizeLower  ∷ !Size
+  , gramSizeUpper  ∷ !Size
+  , useLevenshtein ∷ !Bool
+  , exactSet       ∷ !ExactSet
+  , matchDict      ∷ !MatchDict
+  , items          ∷ !ItemMap
+  } deriving (Eq, Show)
+
+-- | See 'defaultSet'.
+instance Default FuzzySet where
+  def = defaultSet
+
+data GetContext = GetContext
+  { key      ∷ !Text
+  , minScore ∷ !Double
+  , set      ∷ !FuzzySet
+  } deriving (Show)
+
+-- | A 'FuzzySet' with the following field values:
+--
+-- > { gramSizeLower  = 2
+-- > , gramSizeUpper  = 3
+-- > , useLevenshtein = True
+-- > , exactSet       = ε
+-- > , matchDict      = ε
+-- > , items          = ε }
+defaultSet ∷ FuzzySet
+defaultSet = FuzzySet 2 3 True mempty mempty mempty
+
diff --git a/src/Data/FuzzySet/Util.hs b/src/Data/FuzzySet/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/FuzzySet/Util.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE UnicodeSyntax #-}
+module Data.FuzzySet.Util
+  ( distance
+  , enclosedIn
+  , normalized
+  , norm
+  , substr
+  , ε
+  , (<$$>)
+  , (−)
+  , (×)
+  ) where
+
+import Data.Char                       ( isAlphaNum, isSpace )
+import Data.HashMap.Strict             ( HashMap, empty )
+import Data.Text                       ( Text, cons, snoc )
+import Data.Text.Metrics
+import Prelude.Unicode
+
+import qualified Data.Text             as Text
+
+-- | Normalize the input by
+--
+--     * removing non-word characters, except for spaces and commas; and
+--     * converting alphabetic characters to lowercase.
+normalized ∷ Text → Text
+normalized = Text.filter word ∘ Text.toLower
+  where
+    word ch
+      | isAlphaNum ch = True
+      | isSpace    ch = True
+      | (≡) ','    ch = True
+      | otherwise     = False
+
+-- | Return /n/ characters starting from offset /m/ in the input string.
+substr ∷ Int  -- ^ Length of the substring
+       → Int  -- ^ A character offset /m/
+       → Text -- ^ The input string
+       → Text -- ^ A substring of length /n/
+{-# INLINE substr #-}
+substr n m = Text.take n ∘ Text.drop m
+
+-- | Insert the character /ch/ at the beginning and end of the input string.
+enclosedIn ∷ Text → Char → Text
+{-# INLINE enclosedIn #-}
+enclosedIn str ch = ch `cons` str `snoc` ch
+
+-- | Returns the euclidian norm, or /magnitude/, of the input list interpreted 
+--   as a vector. That is, \( \sqrt{ \sum_{i=0}^n a_i^2 } \) for the input 
+--   \( \langle a_0, a_1, \dots, a_n \rangle \) where \( a_i \) is the
+--   element at position /i/ in the input list.
+norm ∷ (Integral a, Floating b) ⇒ [a] → b
+norm = sqrt ∘ fromIntegral ∘ sum ∘ fmap (^2)
+
+-- | Return the normalized Levenshtein distance between the two strings.
+distance ∷ Text → Text → Double
+distance s t = fromRational (toRational d)
+  where
+    d = levenshteinNorm s t
+
+-- | @(\<$$\>) = fmap ∘ fmap@
+(<$$>) ∷ (Functor f, Functor g) ⇒ (a → b) → g (f a) → g (f b)
+(<$$>) = fmap ∘ fmap
+{-# INLINE (<$$>) #-}
+
+-- | Empty HashMap
+ε ∷ HashMap k v
+ε = empty
+{-# INLINE ε #-}
+
+-- | Unicode minus sign symbol. Slightly longer than the hyphen-minus commonly
+--   used, U+2212 aligns naturally with the horizontal bar of the + symbol.
+(−) ∷ Num α ⇒ α → α → α
+(−) = (-)
+{-# INLINE (−) #-}
+
+-- | Another unicode operator. This one for multiplication.
+(×) ∷ Num α ⇒ α → α → α
+(×) = (*)
+{-# INLINE (×) #-}
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,654 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE UnicodeSyntax     #-}
+
+import Control.Exception             ( evaluate )
+import Control.Lens
+import Control.Monad                 ( zipWithM_ )
+import Data.AEq
+import Data.FuzzySet                 hiding ( fromList )
+import Data.FuzzySet.Internal
+import Data.FuzzySet.Lens
+import Data.FuzzySet.Types
+import Data.FuzzySet.Util
+import Data.HashMap.Strict           ( HashMap, fromList, empty )
+import Data.List                     ( sortOn )
+import Data.Maybe
+import Data.Monoid
+import Data.Monoid.Unicode
+import Data.Text                     ( Text, pack, unpack )
+import Prelude.Unicode
+import Test.Hspec
+
+import qualified Data.Text           as Text
+import qualified Data.HashMap.Strict as Map
+
+shouldBeTrue ∷ Bool → Expectation
+shouldBeTrue = shouldBe True
+
+shouldBeCloseTo ∷ Double → Double → Expectation
+shouldBeCloseTo q r = shouldBeTrue (q ~== r)
+
+shouldNotBeCloseTo ∷ Double → Double → Expectation
+shouldNotBeCloseTo q r = shouldBeTrue $ not (q ~== r)
+
+lookup0 ∷ Text → HashMap Text Int → Int
+lookup0 = Map.lookupDefault 0
+
+checkGramsCount ∷ Text → Size → SpecWith ()
+checkGramsCount input n = it message $ do
+    length list `shouldBe` expectedLen
+    shouldBeTrue $ all (≡n) (Text.length <$> list)
+  where
+    list = grams input n
+    message = "should return a list of length "
+            ⊕ show expectedLen
+            ⊕ ", given the input \""
+            ⊕ unpack input ⊕ "\" and n = " ⊕ show n
+    expectedLen = s - n + 3
+    s = Text.length input
+
+checkMapKey ∷ HashMap Text Int → Text → Int → SpecWith ()
+checkMapKey grams key value =
+    it message (lookup0 key grams `shouldBe` value)
+  where
+    message = "should map they key \""
+            ⊕ unpack key ⊕ "\" to "
+            ⊕ show value
+
+vectorMagnitudeOfItem ∷ FuzzySet → Int → Int → Maybe Double
+vectorMagnitudeOfItem set n p = vectorMagnitude <$> set^._items.at n._Just^?ix p
+
+checkMagnitude ∷ FuzzySet → Int → Int → Double → SpecWith ()
+checkMagnitude set n p r =
+    it message $ fromMaybe 0 (vectorMagnitudeOfItem set n p) `shouldBeCloseTo` r
+  where
+    message = "should return vectorMagnitude = " ⊕ show r
+            ⊕ " for the " ⊕ show n ⊕ "-grams entry (index " ⊕ show p ⊕ ")"
+
+checkMatchDictEntry ∷ FuzzySet → Text → [GramInfo] → SpecWith ()
+checkMatchDictEntry set gram entry =
+    it message (set^._matchDict.ix gram `shouldBe` entry)
+  where
+    message = "should return a match dict entry "
+            ⊕ show entry ⊕ " for " ⊕ show gram
+
+checkExactSet ∷ FuzzySet → [(Text, Text)] → SpecWith ()
+checkExactSet set xs =
+    it ("should have exactSet = " ⊕ show xs) $
+      exactSet set `shouldBe` fromList xs
+
+checkGrams ∷ Text → Size → [Text] → SpecWith ()
+checkGrams txt size r =
+    describe msg $ it ("should return " ⊕ show r) (grams txt size `shouldBe` r)
+  where
+    msg = "grams " ⊕ show txt ⊕ " " ⊕ show size
+
+checkGramMap ∷ Text → Size → [(Text, Int)] → SpecWith ()
+checkGramMap txt size r =
+    describe msg $ it ("should return " ⊕ show r)
+                      (gramMap txt size `shouldBe` fromList r)
+  where
+    msg = "gramMap " ⊕ show txt ⊕ " " ⊕ show size
+
+checkGramMapKeys ∷ Text → Size → [(Text, Int)] → SpecWith ()
+checkGramMapKeys txt size keys =
+    describe msg $ mapM_ (uncurry $ checkMapKey grams) keys
+  where
+    msg = "gramMap " ⊕ show txt ⊕ " " ⊕ show size
+    grams = gramMap txt size
+
+checkMatches ∷ FuzzySet → Text → Size → [(Text, Int)] → SpecWith ()
+checkMatches set txt size r =
+    describe msg $ it ("should return " ⊕ show r) (sortOn fst res `shouldBe` sortOn fst r)
+  where
+    res = fmap f $ Map.toList $ matches set (gramMap txt size)
+    f (a, b) = (set ^._items.ix size ^? ix a._normalizedEntry & fromJust, b)
+    msg = "matches "  ⊕ show (set^._exactSet) ⊕ " "
+        ⊕ "(gramMap " ⊕ show txt ⊕ " " ⊕ show size ⊕ ")"
+
+checkGet ∷ FuzzySet → Text → [(Double, Text)] → SpecWith ()
+checkGet set val rs =
+    describe msg $ do
+      it "should return a sorted list" (sorted $ fst <$> xs)
+      it ("should return " ⊕ show (length rs) ⊕ " match(es)")
+         (length rs `shouldBe` length xs)
+      zipWithM_ ξ rs xs -- (sortOn snd rs) (sortOn snd xs)
+  where
+    xs = get set val
+    ξ (a, b) (a', b') = do
+      it ("should return a match for the string " ⊕ show b) (b `shouldBe` b')
+      it ("having a score close to " ⊕ show a) (a `shouldBeCloseTo` a')
+    msg = "get (" ⊕ show (set^._exactSet) ⊕ ") " ⊕ show val
+
+checkDistance ∷ Text → Text → Double → SpecWith ()
+checkDistance s t d =
+    describe msg $
+      it ("should be approximately " ⊕ show d)
+         (distance s t `shouldBeCloseTo` d)
+  where
+    msg = "edit distance between " ⊕ show s ⊕ " and " ⊕ show t
+
+sorted ∷ Ord a ⇒ [a] → Bool
+sorted [ ] = True
+sorted [_] = True
+sorted (x:xs) = x ≥ head xs ∧ sorted xs
+
+main ∷ IO ()
+main = hspec $ do
+
+    describe "grams" $ do
+      mapM_ (checkGramsCount "charade") [2..6]
+      it "should throw an error if n < 2" $
+        evaluate (grams "anything" 1) `shouldThrow` anyException
+
+    checkGrams "charade" 2 ["-c", "ch", "ha", "ar", "ra", "ad", "de", "e-"]
+    checkGrams "charade" 2 ["-c", "ch", "ha", "ar", "ra", "ad", "de", "e-"]
+    checkGrams "charade" 3 ["-ch", "cha", "har", "ara", "rad", "ade", "de-"]
+    checkGrams "aFl1pP!.,nG FL0^ppy+" 2
+        [ "-a", "af", "fl", "l1", "1p", "pp", "p,", ",n", "ng", "g ", " f"
+        , "fl", "l0", "0p", "pp", "py", "y-" ]
+
+    checkGramMap "xxx" 2 [("-x", 1),("xx", 2),("x-", 1)]
+    checkGramMap "xxx" 3 [("-xx", 1), ("xx-", 1), ("xxx", 1)]
+    checkGramMap "xxxxxxx" 4 [("-xxx", 1), ("xxxx", 4), ("xxx-", 1)]
+    checkGramMap "bananasananas" 2
+        [ ("-b", 1), ("ba", 1), ("an", 4), ("na", 4), ("as", 2)
+        , ("sa", 1), ("s-", 1) ]
+    checkGramMap "bananasananas" 3
+        [ ("-ba", 1), ("ban", 1), ("ana", 4), ("nan", 2), ("nas", 2)
+        , ("asa", 1), ("san", 1), ("as-", 1) ]
+
+    checkGramMapKeys "trentsauntsrestaurant" 2
+      [ ("nt", 3)
+      , ("au", 2)
+      , ("ts", 2)
+      , ("re", 2)
+      , ("st", 1)
+      , ("en", 1) ]
+    checkGramMapKeys "trentsauntsrestaurant" 3
+      [ ("res", 1)
+      , ("nts", 2) ]
+    checkGramMapKeys "trentsantwantstorentpants" 3
+      [ ("pan", 1)
+      , ("twa", 1)
+      , ("ant", 3)
+      , ("ren", 2)
+      , ("ent", 2)
+      , ("nts", 3) ]
+    checkGramMapKeys "trentsantwantstorentpantstostartrestaurant" 3
+      [ ("ant", 4)
+      , ("nts", 3)
+      , ("sto", 2)
+      , ("sta", 2)
+      , ("ren", 2)
+      , ("tre", 2) ]
+    checkGramMapKeys "trentsantwantstorentpantstostartrestaurant" 2
+      [ ("an", 4)
+      , ("st", 4)
+      , ("re", 3)
+      , ("ts", 3)
+      , ("en", 2)
+      , ("to", 2)
+      , ("tr", 2)
+      , ("or", 1)
+      , ("au", 1)
+      , ("ur", 1) ]
+    checkGramMapKeys "antsintrentspantswanttrentsauntsrestaurant" 3
+      [ ("nts", 5)
+      , ("ant", 4)
+      , ("ent", 2) ]
+    checkGramMapKeys "asmartantintrentspantswantstorenttrentsauntsrestaurant" 3
+      [ ("nts", 5)
+      , ("ant", 4)
+      , ("ent", 3) ]
+    checkGramMapKeys "buffalo buffalo buffalo buffalo buffalo buffalo" 7
+      [ ("buffalo", 6) ]
+
+    describe "addToSet defaultSet \"aFl1pP!.,nG FL0^ppy+\"" $
+      let (set, changed) = addToSet defaultSet "aFl1pP!.,nG FL0^ppy+"
+       in do
+        it "should return changed status True" $ shouldBeTrue changed
+        checkExactSet set [("afl1pp!.,ng fl0^ppy+", "aFl1pP!.,nG FL0^ppy+")]
+        checkMagnitude set 2 0 4.58257569495584
+        checkMagnitude set 3 0 4.0
+        checkMatchDictEntry set "-a" [GramInfo 0 1]
+        checkMatchDictEntry set "ng" [GramInfo 0 1]
+        checkMatchDictEntry set "fl" [GramInfo 0 2]
+        checkMatchDictEntry set "pp" [GramInfo 0 2]
+        checkMatchDictEntry set "g " [GramInfo 0 1]
+        checkMatchDictEntry set "xx" []
+
+    describe "addToSet defaultSet \"Trent\"" $
+      let (set, changed) = addToSet defaultSet "Trent"
+       in do
+        it "should return changed status True" $ shouldBeTrue changed
+        checkExactSet set [("trent", "Trent")]
+        checkMagnitude set 2 0 2.449489742783178
+        checkMagnitude set 3 0 2.23606797749979
+        checkMatchDictEntry set "en" [GramInfo 0 1]
+
+    describe "defaultSet `add` \"Trent\" `add` \"tent\"" $
+      let set = defaultSet `add` "Trent" `add` "tent"
+       in do
+        checkExactSet set [("trent", "Trent"), ("tent", "tent")]
+        checkMagnitude set 2 0 2.449489742783178
+        checkMagnitude set 2 1 2.23606797749979
+        checkMagnitude set 3 0 2.23606797749979
+        checkMagnitude set 3 1 2.0
+        checkMatchDictEntry set "en"  [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ent" [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ten" [GramInfo 1 1]
+        checkMatchDictEntry set "-t"  [GramInfo 0 1, GramInfo 1 1]
+
+    describe "defaultSet `add` \"Trent\" `add` \"tent\" `add` \"restaurant\"" $
+      let set = defaultSet `add` "Trent" `add` "tent" `add` "restaurant"
+       in do
+        checkExactSet set
+          [ ("trent"      , "Trent")
+          , ("tent"       , "tent")
+          , ("restaurant" , "restaurant") ]
+        checkMagnitude set 2 0 2.449489742783178
+        checkMagnitude set 2 1 2.23606797749979
+        checkMagnitude set 2 2 3.3166247903554
+        checkMagnitude set 3 0 2.23606797749979
+        checkMagnitude set 3 1 2.0
+        checkMagnitude set 3 2 3.1622776601683795
+        checkMatchDictEntry set "tau" [GramInfo 2 1]
+        checkMatchDictEntry set "en"  [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ten" [GramInfo 1 1]
+        checkMatchDictEntry set "ran" [GramInfo 2 1]
+        checkMatchDictEntry set "an"  [GramInfo 2 1]
+        checkMatchDictEntry set "ant" [GramInfo 2 1]
+        checkMatchDictEntry set "nt-" [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1]
+        checkMatchDictEntry set "st"  [GramInfo 2 1]
+        checkMatchDictEntry set "es"  [GramInfo 2 1]
+        checkMatchDictEntry set "est" [GramInfo 2 1]
+        checkMatchDictEntry set "re"  [GramInfo 0 1, GramInfo 2 1]
+        checkMatchDictEntry set "-tr" [GramInfo 0 1]
+        checkMatchDictEntry set "res" [GramInfo 2 1]
+        checkMatchDictEntry set "tr"  [GramInfo 0 1]
+        checkMatchDictEntry set "-t"  [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "aur" [GramInfo 2 1]
+        checkMatchDictEntry set "ent" [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ra"  [GramInfo 2 1]
+        checkMatchDictEntry set "-r"  [GramInfo 2 1]
+        checkMatchDictEntry set "ren" [GramInfo 0 1]
+        checkMatchDictEntry set "te"  [GramInfo 1 1]
+        checkMatchDictEntry set "nt"  [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1]
+
+    describe "defaultSet `add` \"Trent\" `add` \"tent\" `add` \"restaurant\" `add` \"xRftAntnt,!tnRant\"" $
+      let set = defaultSet `add` "Trent" `add` "tent" `add` "restaurant" `add` "xRftAntnt,!tnRant"
+       in do
+        checkExactSet set
+          [ ("trent"             , "Trent")
+          , ("tent"              , "tent")
+          , ("restaurant"        , "restaurant")
+          , ("xrftantnt,!tnrant" , "xRftAntnt,!tnRant") ]
+        checkMagnitude set 2 0 2.449489742783178
+        checkMagnitude set 2 1 2.23606797749979
+        checkMagnitude set 2 2 3.3166247903554
+        checkMagnitude set 2 3 5.196152422706632
+        checkMagnitude set 3 0 2.23606797749979
+        checkMagnitude set 3 1 2.0
+        checkMagnitude set 3 2 3.1622776601683795
+        checkMagnitude set 3 3 4.242640687119285
+        checkMatchDictEntry set "tau" [GramInfo 2 1]
+        checkMatchDictEntry set "en"  [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ten" [GramInfo 1 1]
+        checkMatchDictEntry set "ran" [GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "ntn" [GramInfo 3 1]
+        checkMatchDictEntry set "-xr" [GramInfo 3 1]
+        checkMatchDictEntry set "an"  [GramInfo 2 1, GramInfo 3 2]
+        checkMatchDictEntry set "ant" [GramInfo 2 1, GramInfo 3 2]
+        checkMatchDictEntry set "t,t" [GramInfo 3 1]
+        checkMatchDictEntry set "nt-" [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "nt," [GramInfo 3 1]
+        checkMatchDictEntry set "xr"  [GramInfo 3 1]
+        checkMatchDictEntry set "tan" [GramInfo 3 1]
+        checkMatchDictEntry set "st"  [GramInfo 2 1]
+        checkMatchDictEntry set "es"  [GramInfo 2 1]
+        checkMatchDictEntry set "fta" [GramInfo 3 1]
+        checkMatchDictEntry set "est" [GramInfo 2 1]
+        checkMatchDictEntry set "re"  [GramInfo 0 1, GramInfo 2 1]
+        checkMatchDictEntry set "-tr" [GramInfo 0 1]
+        checkMatchDictEntry set "res" [GramInfo 2 1]
+        checkMatchDictEntry set "xrf" [GramInfo 3 1]
+        checkMatchDictEntry set "tr"  [GramInfo 0 1]
+        checkMatchDictEntry set ",t"  [GramInfo 3 1]
+        checkMatchDictEntry set "tn"  [GramInfo 3 2]
+        checkMatchDictEntry set "-t"  [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "rf"  [GramInfo 3 1]
+        checkMatchDictEntry set "aur" [GramInfo 2 1]
+        checkMatchDictEntry set "ent" [GramInfo 0 1, GramInfo 1 1]
+        checkMatchDictEntry set "ra"  [GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "-r"  [GramInfo 2 1]
+        checkMatchDictEntry set "-r"  [GramInfo 2 1]
+        checkMatchDictEntry set "ren" [GramInfo 0 1]
+        checkMatchDictEntry set "nr"  [GramInfo 3 1]
+        checkMatchDictEntry set "te"  [GramInfo 1 1]
+        checkMatchDictEntry set "nt"  [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1, GramInfo 3 3]
+        checkMatchDictEntry set "-x"  [GramInfo 3 1]
+        checkMatchDictEntry set "ta"  [GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "ft"  [GramInfo 3 1]
+        checkMatchDictEntry set "nra" [GramInfo 3 1]
+        checkMatchDictEntry set ",tn" [GramInfo 3 1]
+        checkMatchDictEntry set "-re" [GramInfo 2 1]
+        checkMatchDictEntry set "ura" [GramInfo 2 1]
+        checkMatchDictEntry set "tnt" [GramInfo 3 1]
+        checkMatchDictEntry set "sta" [GramInfo 2 1]
+        checkMatchDictEntry set "tnr" [GramInfo 3 1]
+        checkMatchDictEntry set "rft" [GramInfo 3 1]
+        checkMatchDictEntry set "tre" [GramInfo 0 1]
+        checkMatchDictEntry set "ur"  [GramInfo 2 1]
+        checkMatchDictEntry set "t,"  [GramInfo 3 1]
+        checkMatchDictEntry set "t-"  [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "au"  [GramInfo 2 1]
+        checkMatchDictEntry set "-te" [GramInfo 1 1]
+
+    describe "FuzzySet 3 4 True mempty mempty mempty `add` ..." $
+      let set = FuzzySet 3 4 True mempty mempty mempty
+                            `add` "Trent"
+                            `add` "pants"
+                            `add` "restaurant"
+                            `add` "XrF,!TNrATaNTNTNT"
+       in do
+        checkExactSet set
+          [ ("trent"             , "Trent")
+          , ("pants"             , "pants")
+          , ("restaurant"        , "restaurant")
+          , ("xrf,!tnratantntnt" , "XrF,!TNrATaNTNTNT") ]
+        checkMagnitude set 3 0 2.23606797749979
+        checkMagnitude set 3 1 2.23606797749979
+        checkMagnitude set 3 2 3.1622776601683795
+        checkMagnitude set 3 3 4.47213595499958
+        checkMagnitude set 4 0 2.0
+        checkMagnitude set 4 1 2.0
+        checkMagnitude set 4 2 3.0
+        checkMagnitude set 4 3 4.123105625617661
+        checkMatchDictEntry set "ntnt" [GramInfo 3 2]
+        checkMatchDictEntry set "tau"  [GramInfo 2 1]
+        checkMatchDictEntry set "xrf"  [GramInfo 3 1]
+        checkMatchDictEntry set "esta" [GramInfo 2 1]
+        checkMatchDictEntry set "-pa"  [GramInfo 1 1]
+        checkMatchDictEntry set "ran"  [GramInfo 2 1]
+        checkMatchDictEntry set "ntn"  [GramInfo 3 2]
+        checkMatchDictEntry set "-xr"  [GramInfo 3 1]
+        checkMatchDictEntry set "ants" [GramInfo 1 1]
+        checkMatchDictEntry set "-xrf" [GramInfo 3 1]
+        checkMatchDictEntry set "ant"  [GramInfo 1 1, GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "rant" [GramInfo 2 1]
+        checkMatchDictEntry set "rat"  [GramInfo 3 1]
+        checkMatchDictEntry set "antn" [GramInfo 3 1]
+        checkMatchDictEntry set "nt-"  [GramInfo 0 1, GramInfo 2 1, GramInfo 3 1]
+        checkMatchDictEntry set "rent" [GramInfo 0 1]
+        checkMatchDictEntry set "rata" [GramInfo 3 1]
+        checkMatchDictEntry set "tan"  [GramInfo 3 1]
+        checkMatchDictEntry set "tant" [GramInfo 3 1]
+        checkMatchDictEntry set "-tre" [GramInfo 0 1]
+        checkMatchDictEntry set "est"  [GramInfo 2 1]
+        checkMatchDictEntry set "-tr"  [GramInfo 0 1]
+
+    describe "FuzzySet 2 5 True mempty mempty mempty `add` ..." $
+      let set = FuzzySet 2 5 True mempty mempty mempty
+                            `add` "Trent"
+                            `add` "restaurant"
+                            `add` "aunt"
+                            `add` "Smarty Pants"
+                            `add` "XrF,!TNrATaNTNTNT"
+       in do
+        checkExactSet set
+          [ ("trent"             , "Trent")
+          , ("restaurant"        , "restaurant")
+          , ("aunt"              , "aunt")
+          , ("smarty pants"      , "Smarty Pants")
+          , ("xrf,!tnratantntnt" , "XrF,!TNrATaNTNTNT") ]
+        checkMagnitude set 2 0 2.449489742783178
+        checkMagnitude set 2 1 3.3166247903554
+        checkMagnitude set 2 2 2.23606797749979
+        checkMagnitude set 2 3 3.605551275463989
+        checkMagnitude set 2 4 5.385164807134504
+        checkMagnitude set 3 0 2.23606797749979
+        checkMagnitude set 3 1 3.1622776601683795
+        checkMagnitude set 3 2 2.0
+        checkMagnitude set 3 3 3.4641016151377544
+        checkMagnitude set 3 4 4.47213595499958
+        checkMagnitude set 4 0 2.0
+        checkMagnitude set 4 1 3.0
+        checkMagnitude set 4 2 1.7320508075688772
+        checkMagnitude set 4 3 3.3166247903554
+        checkMagnitude set 4 4 4.123105625617661
+        checkMagnitude set 5 0 1.7320508075688772
+        checkMagnitude set 5 1 2.8284271247461903
+        checkMagnitude set 5 2 1.4142135623730951
+        checkMagnitude set 5 3 3.1622776601683795
+        checkMagnitude set 5 4 3.7416573867739413
+        checkMatchDictEntry set "pant"  [GramInfo 3 1]
+        checkMatchDictEntry set "y "    [GramInfo 3 1]
+        checkMatchDictEntry set "-xr"   [GramInfo 4 1]
+        checkMatchDictEntry set "rest"  [GramInfo 1 1]
+        checkMatchDictEntry set " p"    [GramInfo 3 1]
+        checkMatchDictEntry set "ty p"  [GramInfo 3 1]
+        checkMatchDictEntry set "rty"   [GramInfo 3 1]
+        checkMatchDictEntry set "-tre"  [GramInfo 0 1]
+        checkMatchDictEntry set "-a"    [GramInfo 2 1]
+        checkMatchDictEntry set "ty"    [GramInfo 3 1]
+        checkMatchDictEntry set "tntnt" [GramInfo 4 1]
+        checkMatchDictEntry set "tr"    [GramInfo 0 1]
+        checkMatchDictEntry set "ts"    [GramInfo 3 1]
+        checkMatchDictEntry set "aun"   [GramInfo 2 1]
+        checkMatchDictEntry set "tn"    [GramInfo 4 3]
+        checkMatchDictEntry set "-t"    [GramInfo 0 1]
+        checkMatchDictEntry set "aur"   [GramInfo 1 1]
+        checkMatchDictEntry set "-s"    [GramInfo 3 1]
+        checkMatchDictEntry set "-r"    [GramInfo 1 1]
+        checkMatchDictEntry set "rty"   [GramInfo 3 1]
+        checkMatchDictEntry set "tnra"  [GramInfo 4 1]
+        checkMatchDictEntry set "nt"    [GramInfo 0 1, GramInfo 1 1, GramInfo 2 1, GramInfo 3 1, GramInfo 4 3]
+
+    describe "values (defaultSet `add` ...)" $ do
+      let set = defaultSet `add` "Trent" `add` "restaurant"
+                           `add` "aunt"  `add` "Smarty Pants"
+                           `add` "XrF,!TNrATaNTNTNT"
+      it "should contain the added elements" $ do
+        values set `shouldContain` ["Trent"]
+        values set `shouldContain` ["restaurant"]
+        values set `shouldContain` ["aunt"]
+        values set `shouldContain` ["Smarty Pants"]
+        values set `shouldContain` ["XrF,!TNrATaNTNTNT"]
+
+    describe "size (defaultSet `add` ...)" $ do
+      let set = defaultSet `add` "Trent" `add` "restaurant"
+                           `add` "aunt"  `add` "Smarty Pants"
+                           `add` "XrF,!TNrATaNTNTNT"
+      it "should be 5" $ size set `shouldBe` 5
+
+    describe "isEmpty (defaultSet `add` ...)" $ do
+      let set = defaultSet `add` "Trent" `add` "restaurant"
+                           `add` "aunt"  `add` "Smarty Pants"
+                           `add` "XrF,!TNrATaNTNTNT"
+      it "should be False" $ isEmpty set `shouldBe` False
+
+    describe "isEmpty defaultSet" $ do
+      let set = defaultSet
+      it "should be True" $ isEmpty set `shouldBe` True
+
+    describe "snd $ (defaultSet `add` \"again\") `addToSet` \"again\"" $ do
+      let set = (defaultSet `add` "again") `add` "again"
+      it "should return False" $
+        snd ((defaultSet `add` "again") `addToSet` "again") `shouldBe` False
+
+    describe "get (defaultSet `add` \"xxx\")" $ do
+      let set = defaultSet `add` "xxx"
+      it "should return [(1, \"xxx\")]" $
+        get set "xxx" `shouldBe` [(1, "xxx")]
+
+    checkMatches testset_1 "ant"   3 [("trent", 1), ("restaurant", 2), ("aunt", 1), ("smarty pants", 1)]
+    checkMatches testset_1 "pant"  3 [("trent", 1), ("restaurant", 2), ("aunt", 1), ("smarty pants", 2)]
+    checkMatches testset_1 "pants" 3 [("restaurant", 1), ("smarty pants", 4)]
+    checkMatches testset_1 "tre"   3 [("trent", 2)]
+    checkMatches testset_1 "xxx"   3 []
+    checkMatches testset_1 "xxx"   2 []
+    checkMatches testset_1 "tsap"  3 []
+    checkMatches testset_1 "tsap"  2 [("trent", 1), ("smarty pants", 1)]
+    checkMatches testset_2 "hat"   3 [("cat", 1)]
+    checkMatches testset_2 "anthropology" 3 [("restaurant", 1), ("smarty pants", 1)]
+    checkMatches testset_2 "spot"  3 []
+    checkMatches testset_2 "spot"  2 [("trent", 1), ("restaurant", 1), ("aunt", 1), ("smarty pants", 1), ("cat", 1)]
+    checkMatches testset_2 "axiom" 3 []
+    checkMatches testset_2 "axiom" 2 [("aunt", 1)]
+    checkMatches testset_3 "moped" 2 [("polymorphic", 1)]
+    checkMatches (defaultSet `add` "bananas") "ananas" 3 [("bananas", 7)]
+    checkMatches (defaultSet `add` "banana")  "ananas" 3 [("banana", 5)]
+    checkMatches testset_6  "ia" 3 [("california", 1), ("district of columbia", 1), ("georgia", 1), ("pennsylvania", 1), ("virginia", 1), ("west virginia", 1)]
+    checkMatches testset_6  "was" 3 [("arkansas", 1), ("kansas", 1), ("texas", 1), ("washington", 2)]
+    checkMatches testset_6  "ton" 3 [("oregon", 1), ("washington", 2)]
+    checkMatches testset_6  "ing" 3 [("indiana", 1), ("washington", 1), ("wyoming", 2)]
+    checkMatches testset_6  "land" 3 [("maryland", 3), ("northern marianas islands", 2), ("rhode island", 3), ("virgin islands", 2)]
+    checkMatches testset_6  "sas" 3 [("arkansas", 2), ("kansas", 2), ("texas", 1)]
+    checkMatches testset_6  "sin" 3 [("wisconsin", 2)]
+    checkMatches testset_6  "new" 3 [("nebraska", 1), ("nevada", 1), ("new hampshire", 2), ("new jersey", 2), ("new mexico", 2), ("new york", 2)]
+
+    -- Tests where useLevenshtein == False
+    checkGet testset_4 "flask"    [(0.3651483716701107, "Alaska")]
+    checkGet testset_4 "lambda"   [(0.40089186286863654, "Alabama")]
+    checkGet testset_4 "lambada"  [(0.49999999999999999, "Alabama")]
+    checkGet testset_4 "alabama"  [(1, "Alabama")]
+    checkGet testset_4 "al"       [(0.4364357804719848, "Alaska"), (0.40824829046386296, "Alabama")]
+    checkGet testset_4 "albama"   [(0.6172133998483676, "Alabama")]
+    checkGet testset_4 "Alabaska" [ (0.7216878364870323, "Alaska")
+                                  , (0.5345224838248487, "Alabama") ]
+    checkGet testset_5 "homeland"     [(0.37499999999999994, "Maryland")]
+    checkGet testset_5 "connectedcut" [(0.6963106238227914, "Connecticut")]
+    checkGet testset_5 "oregano"      [(0.4629100498862757, "Oregon")]
+    checkGet testset_5 "akeloxasas"   [(0.4622501635210243, "Arkansas"), (0.45291081365783836, "Texas"), (0.4193139346887673, "Kansas")]
+    checkGet testset_5 "alaskansas"   [ (0.6454972243679029, "Kansas")
+                                      , (0.6454972243679029, "Alaska")
+                                      , (0.5590169943749475, "Arkansas") ]
+    checkGet testset_5 "South"        [ (0.5163977794943222, "South Dakota" )
+                                      , (0.47809144373375745, "South Carolina") ]
+    checkGet testset_5 "penicillivania" [ (0.46291004988627577, "Pennsylvania") ]
+    checkGet testset_5 "Michisota"    [ (0.4714045207910316, "Michigan")
+                                      , (0.4444444444444444, "Minnesota") ]
+    checkGet testset_5 "New Mix"      [ (0.47809144373375745, "New Mexico")
+                                      , (0.40089186286863654, "New York")
+                                      , (0.35856858280031806, "New Jersey") ]
+    checkGet testset_5 "Waioming"     [ (0.5345224838248487, "Wyoming")]
+    checkGet testset_5 "Landland"     [ (0.5103103630798287, "Maryland")
+                                      , (0.41666666666666674, "Rhode Island") ]
+
+    checkDistance "hello" "yello" 0.8
+    checkDistance "fellow" "yello" 0.6666666666666667
+    checkDistance "fellow" "yellow" 0.8333333333333334
+    checkDistance "propeller" "yellow" 0.33333333333333337
+    checkDistance "propeller" "teller" 0.5555555555555556
+    checkDistance "balloon" "spoon" 0.4285714285714286
+    checkDistance "balloon" "electron" 0.25
+    checkDistance "spectrum" "electron" 0.5
+    checkDistance "spectrum" "techno" 0.25
+    checkDistance "technology" "techno" 0.6
+    checkDistance "technology" "logic" 0.19999999999999996
+    checkDistance "toxic" "logic" 0.6
+    checkDistance "sawa" "sawa" 1
+    checkDistance "fez" "baz" 0.33333333333333337
+    -- Just a sanity check
+    describe "edit distance between \"fez\" and \"baz\"" $
+      it ("should not be close to 0.123")
+         (distance "fez" "baz" `shouldNotBeCloseTo` 0.123)
+
+    -- Tests where useLevenshtein == True
+    checkGet testset_6 "wyome" [ (0.5714285714285714, "Wyoming") ]
+    checkGet testset_6 "Louisianaland" [ ( 0.6923076923076923, "Louisiana" )
+                                       , ( 0.3846153846153846, "Maryland" )
+                                       , ( 0.3846153846153846, "Rhode Island" )
+                                       , ( 0.36, "Northern Marianas Islands" ) ]
+    checkGet testset_6 "ia" [ (0.5, "Iowa"), (0.4, "Idaho") ]
+    checkGet testset_6 "flaska" [ (0.8333333333333334, "Alaska")
+                                , (0.5, "Nebraska")
+                                , (0.4285714285714286, "Florida") ]
+    checkGet testset_7 "Alaskansas" [ (0.7, "Arkansas")
+                                    , (0.6, "Kansas")
+                                    , (0.6, "Alaska")
+                                    , (0.5, "Alabama") ]
+    checkGet testset_7 "Transylvania" [ (0.75, "Pennsylvania")
+                                      , (0.33333333333333337, "California") ]
+
+testset_1 ∷ FuzzySet
+testset_1 = defaultSet `add` "Trent" `add` "restaurant"
+                       `add` "aunt"  `add` "Smarty Pants"
+testset_2 ∷ FuzzySet
+testset_2 = testset_1 `add` "cat"
+
+testset_3 ∷ FuzzySet
+testset_3 = testset_2 `add` "polymorphic"
+
+testset_4 ∷ FuzzySet
+testset_4 = FuzzySet 2 3 False empty empty empty
+  `add` "Alaska" `add` "Alabama" `add` "Guam"
+
+testset_5 ∷ FuzzySet
+testset_5 = addMany (FuzzySet 2 3 False empty empty empty) states
+
+testset_6 ∷ FuzzySet
+testset_6 = addMany defaultSet states
+
+testset_7 ∷ FuzzySet
+testset_7 = addMany (FuzzySet 2 4 True empty empty empty) states
+
+states ∷ [Text]
+states =
+  [ "Alabama"
+  , "Alaska"
+  , "American Samoa"
+  , "Arizona"
+  , "Arkansas"
+  , "California"
+  , "Colorado"
+  , "Connecticut"
+  , "Delaware"
+  , "District of Columbia"
+  , "Florida"
+  , "Georgia"
+  , "Guam"
+  , "Hawaii"
+  , "Idaho"
+  , "Illinois"
+  , "Indiana"
+  , "Iowa"
+  , "Kansas"
+  , "Kentucky"
+  , "Louisiana"
+  , "Maine"
+  , "Maryland"
+  , "Massachusetts"
+  , "Michigan"
+  , "Minnesota"
+  , "Mississippi"
+  , "Missouri"
+  , "Montana"
+  , "Nebraska"
+  , "Nevada"
+  , "New Hampshire"
+  , "New Jersey"
+  , "New Mexico"
+  , "New York"
+  , "North Carolina"
+  , "North Dakota"
+  , "Northern Marianas Islands"
+  , "Ohio"
+  , "Oklahoma"
+  , "Oregon"
+  , "Pennsylvania"
+  , "Puerto Rico"
+  , "Rhode Island"
+  , "South Carolina"
+  , "South Dakota"
+  , "Tennessee"
+  , "Texas"
+  , "Utah"
+  , "Vermont"
+  , "Virginia"
+  , "Virgin Islands"
+  , "Washington"
+  , "West Virginia"
+  , "Wisconsin"
+  , "Wyoming" ]
