diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) Hal Daume III
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. Neither the name of the author nor the names of his contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE 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 AUTHORS 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/NLP/WordNet.hs b/NLP/WordNet.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet.hs
@@ -0,0 +1,338 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  NLP.WordNet
+-- Copyright   :  (c) Hal Daume III 2003-2004
+-- License     :  BSD-style
+--
+-- Maintainer  :  hdaume@isi.edu
+-- Stability   :  experimental
+-- Portability :  non-portable (H98 + implicit parameters)
+--
+-- This is the top level module to the Haskell WordNet interface.
+--
+-- This module is maintained at:
+--    <http://www.isi.edu/~hdaume/HWordNet/>.
+--
+-- This is the only module in the WordNet package you need to import.
+-- The others provide utility functions and primitives that this
+-- module is based on.
+--
+-- More information about WordNet is available at:
+--    <http://http://www.cogsci.princeton.edu/~wn/>.
+-----------------------------------------------------------------------------
+module NLP.WordNet
+    (
+     -- * The basic type system
+     module NLP.WordNet.Types,
+
+     -- * Top level execution functions
+     runWordNet,
+     runWordNetQuiet,
+     runWordNetWithOptions,
+
+     -- * Functions to manually initialize the WordNet system; these are not
+     --   needed if you use one of the "runWordNet" functions above.
+     initializeWordNet,
+     initializeWordNetWithOptions,
+     closeWordNet,
+     runs,
+
+     -- * The basic database access functions.
+     getOverview,
+     searchByOverview,
+     search,
+     lookupKey,
+
+     -- * The agglomeration functions
+     relatedBy,
+     closure,
+     closureOn,
+
+     -- * Computing lowest-common ancestor functions; the implementation
+     --   of these can be tuned by providing a different "Bag" implementation.
+     --   use "emptyQueue" for breadth-first-search (recommended) or "emptyStack"
+     --   for depth-first-search, or write your own.
+     meet,
+     meetPaths,
+     meetSearchPaths,
+     Bag(..),
+     emptyQueue,
+     emptyStack,
+    )
+    where
+
+import Prelude hiding (catch)
+import Data.Array
+import GHC.Arr (unsafeIndex)
+import GHC.IO.Handle
+import Data.Tree
+import Data.IORef
+import Data.Dynamic
+import qualified Data.Set as Set
+import Numeric (readHex, readDec)
+import System.IO.Unsafe
+
+import NLP.WordNet.Common
+import NLP.WordNet.Consts
+import NLP.WordNet.Util
+import NLP.WordNet.Types
+
+import qualified NLP.WordNet.PrimTypes as T
+import qualified NLP.WordNet.Prims     as P
+
+-- | Takes a WordNet command, initializes the environment
+-- and returns the results in the 'IO' monad.  WordNet
+-- warnings are printed to stderr.
+runWordNet :: WN a -> IO a
+runWordNet = runWordNetWithOptions Nothing Nothing
+
+-- | Takes a WordNet command, initializes the environment
+-- and returns the results in the 'IO' monad.  WordNet
+-- warnings are ignored.
+runWordNetQuiet :: WN a -> IO a
+runWordNetQuiet = runWordNetWithOptions Nothing (Just (\_ _ -> return ()))
+
+-- | Takes a FilePath to the directory holding WordNet and
+-- a function to do with warnings and a WordNet command, initializes 
+-- the environment and returns the results in the 'IO' monad.
+runWordNetWithOptions :: 
+    Maybe FilePath ->                          -- word net data directory
+    Maybe (String -> SomeException -> IO ()) ->    -- warning function (by default, warnings go to stderr)
+    WN a ->                                    -- what to run
+      IO a
+runWordNetWithOptions dd warn wn = do
+  wne <- P.initializeWordNetWithOptions dd warn
+  let a = let ?wne = wne in wn
+  -- P.closeWordNet wne
+  return a
+
+-- | Gives you a 'WordNetEnv' which can be passed to 'runs' or used
+-- as the implicit parameter to the other WordNet functions.
+initializeWordNet :: IO WordNetEnv
+initializeWordNet = P.initializeWordNet
+
+-- | Takes a FilePath to the directory holding WordNet and
+-- a function to do with warnings, initializes 
+-- the environment and returns a 'WordNetEnv' as in 'initializeWordNet'.
+initializeWordNetWithOptions :: Maybe FilePath -> Maybe (String -> SomeException -> IO ()) -> IO WordNetEnv
+initializeWordNetWithOptions = P.initializeWordNetWithOptions
+
+-- | Closes all the handles associated with the 'WordNetEnv'.  Since
+-- the functions provided in the "NLP.WordNet.WordNet" module
+-- are /lazy/, you shouldn't do this until you're really done.
+-- Or perhaps not at all (GC will eventually kick in).
+closeWordNet :: WordNetEnv -> IO ()
+closeWordNet = P.closeWordNet
+
+-- | This simply takes a 'WordNetEnv' and provides it as the
+-- implicit parameter to the WordNet command.
+runs :: WordNetEnv -> WN a -> a
+runs wne x = let ?wne = wne in x
+
+
+-- | This takes a word and returns an 'Overview' of all its senses
+-- for all parts of speech.
+getOverview :: WN (Word -> Overview)
+getOverview word = unsafePerformIO $ do
+  idxN <- unsafeInterleaveIO $ getOverview' Noun
+  idxV <- unsafeInterleaveIO $ getOverview' Verb
+  idxA <- unsafeInterleaveIO $ getOverview' Adj
+  idxR <- unsafeInterleaveIO $ getOverview' Adv
+  return (T.Overview idxN idxV idxA idxR)
+  where
+    getOverview' pos = do
+      strM <- P.getIndexString ?wne word pos
+      case strM of
+        Nothing -> return Nothing
+        Just  s -> unsafeInterleaveIO $ P.indexLookup ?wne word pos
+
+-- | This takes an 'Overview' (see 'getOverview'), a 'POS' and a 'SenseType' and returns
+-- a list of search results.  If 'SenseType' is 'AllSenses', there will be one
+-- 'SearchResult' in the results for each valid sense.  If 'SenseType' is
+-- a single sense number, there will be at most one element in the result list.
+searchByOverview :: WN (Overview -> POS -> SenseType -> [SearchResult])
+searchByOverview overview pos sense = unsafePerformIO $ 
+  case (case pos of { Noun -> T.nounIndex ; Verb -> T.verbIndex ; Adj -> T.adjIndex ; Adv -> T.advIndex })
+          overview of
+    Nothing  -> return []
+    Just idx -> do
+      let numSenses = T.indexSenseCount idx
+      skL <- mapMaybe id `liftM` 
+             unsafeInterleaveIO (
+               mapM (\sense -> do
+                     skey <- P.indexToSenseKey ?wne idx sense
+                     return (liftM ((,) sense) skey)
+                    ) (sensesOf numSenses sense)
+             )
+      r <- unsafeInterleaveIO $ mapM (\ (snum, skey) ->
+                 unsafeInterleaveIO (P.getSynsetForSense ?wne skey) >>= \v ->
+                 case v of
+                   Nothing -> return Nothing
+                   Just ss -> return $ Just (T.SearchResult 
+                                               (Just skey) 
+                                               (Just overview) 
+                                               (Just idx)
+                                               (Just (SenseNumber snum)) 
+                                               ss)
+                ) skL
+      return (mapMaybe id r)
+
+-- | This takes a 'Word', a 'POS' and a 'SenseType' and returns
+-- the equivalent of first running 'getOverview' and then 'searchByOverview'.
+search :: WN (Word -> POS -> SenseType -> [SearchResult])
+search word pos sense = searchByOverview (getOverview word) pos sense
+
+-- | This takes a 'Key' (see 'srToKey' and 'srFormKeys') and looks it
+-- up in the databse.
+lookupKey :: WN (Key -> SearchResult)
+lookupKey (T.Key (o,p)) = unsafePerformIO $ do
+  ss <- unsafeInterleaveIO $ P.readSynset ?wne p o ""
+  return $ T.SearchResult Nothing Nothing Nothing Nothing ss
+
+-- | This takes a 'Form' and a 'SearchResult' and returns all
+-- 'SearchResult' related to the given one by the given 'Form'.
+--
+-- For example:
+--
+-- > relatedBy Antonym (head (search "happy" Adj 1))
+-- > [<unhappy>]
+-- >
+-- > relatedBy Hypernym (head (search "dog" Noun 1))
+-- > [<canine canid>]
+relatedBy :: WN (Form -> SearchResult -> [SearchResult])
+relatedBy form sr = map lookupKey $ srFormKeys sr form
+
+-- | This is a utility function to build lazy trees from a function and a root.
+closure :: (a -> [a]) -> a -> Tree a
+closure f x = Node x (map (closure f) $ f x)
+
+-- | This enables 'Form'-based trees to be built.
+--
+-- For example:
+--
+-- > take 5 $ flatten $ closureOn Antonym (head (search "happy" Adj AllSenses)))
+-- > [<happy>,<unhappy>,<happy>,<unhappy>,<happy>]
+--
+-- > closureOn Hypernym (head (search "dog" Noun 1)))
+-- > - <dog domestic_dog Canis_familiaris> --- <canine canid> --- <carnivore>\\
+-- >   --- <placental placental_mammal eutherian eutherian_mammal> --- <mammal>\\
+-- >   --- <vertebrate craniate> --- <chordate> --- <animal animate_being beast\\
+-- >   brute creature fauna> --- <organism being> --- <living_thing animate_thing>\\
+-- >   --- <object physical_object> --- <entity> 
+closureOn :: WN (Form -> SearchResult -> Tree SearchResult)
+closureOn form = closure (relatedBy form)
+
+-- | A simple bag class for our 'meet' implementation.
+class Bag b a where
+  emptyBag :: b a
+  addToBag :: b a -> a -> b a
+  addListToBag :: b a -> [a] -> b a
+  isEmptyBag :: b a -> Bool
+  splitBag :: b a -> (a, b a)
+  addListToBag = foldr (flip addToBag)
+
+instance Bag [] a where
+  emptyBag = []
+  addToBag = flip (:)
+  isEmptyBag = null
+  splitBag (x:xs) = (x, xs)
+
+-- | A very slow queue based on lists.
+newtype Queue a = Queue [a] deriving (Show)
+
+instance Bag Queue a where
+  emptyBag = Queue []
+  addToBag (Queue l) a = Queue (l++[a])
+  isEmptyBag (Queue l) = null l
+  splitBag (Queue (x:xs)) = (x, Queue xs)
+  addListToBag (Queue l) l' = Queue (l ++ l')
+
+-- | An empty stack.
+emptyStack :: [a]
+emptyStack = []
+
+-- | An empty queue.
+emptyQueue :: Queue a
+emptyQueue = Queue []
+
+-- | This function takes an empty bag (in particular, this is to specify
+-- what type of search to perform), and the results of two search.
+-- It returns (maybe) the lowest point at which the two terms
+-- meet in the WordNet hierarchy.
+--
+-- For example:
+--
+-- > meet emptyQueue (head $ search "cat" Noun 1) (head $ search "dog" Noun 1)
+-- > Just <carnivore>
+--
+-- > meet emptyQueue (head $ search "run" Verb 1) (head $ search "walk" Verb 1)
+-- > Just <travel go move locomote>
+meet :: Bag b (Tree SearchResult) => WN (b (Tree SearchResult) -> SearchResult -> SearchResult -> Maybe SearchResult)
+meet emptyBg sr1 sr2 = srch Set.empty Set.empty (addToBag emptyBg t1) (addToBag emptyBg t2)
+  where
+    t1 = closureOn Hypernym sr1
+    t2 = closureOn Hypernym sr2
+
+    srch v1 v2 bag1 bag2
+        | isEmptyBag bag1 && isEmptyBag bag2 = Nothing
+        | isEmptyBag bag1                    = srch v2 v1 bag2 bag1
+        | otherwise = 
+            let (Node sr chl, bag1') = splitBag bag1
+            in  if v2 `containsResult` sr 
+                  then Just sr
+                  else srch v2 (addResult v1 sr) bag2 (addListToBag bag1' chl) -- flip the order :)
+
+    containsResult v sr = srWords sr AllSenses `Set.member` v
+    addResult v sr = Set.insert (srWords sr AllSenses) v
+
+-- | This function takes an empty bag (see 'meet'), and the results of two searches.
+-- It returns (maybe) the lowest point at which the two terms
+-- meet in the WordNet hierarchy, as well as the paths leading from each
+-- term to this common term.
+--
+-- For example:
+--
+-- > meetPaths emptyQueue (head $ search "cat" Noun 1) (head $ search "dog" Noun 1)
+-- > Just ([<cat true_cat>,<feline felid>],<carnivore>,[<canine canid>,<dog domestic_dog Canis_familiaris>])
+--
+-- > meetPaths emptyQueue (head $ search "run" Verb 1) (head $ search "walk" Verb 1)
+-- > Just ([<run>,<travel_rapidly speed hurry zip>],<travel go move locomote>,[<walk>])
+--
+-- This is marginally less efficient than just using 'meet', since it uses
+-- linear-time lookup for the visited sets, whereas 'meet' uses log-time
+-- lookup.
+meetPaths :: Bag b (Tree SearchResult) => 
+             WN (
+                 b (Tree SearchResult) ->       -- bag implementation
+                 SearchResult ->                -- word 1
+                 SearchResult ->                -- word 2
+                 Maybe ([SearchResult], SearchResult, [SearchResult]))   -- word 1 -> common,
+                                                                         -- common
+                                                                         -- common -> word 2
+meetPaths emptyBg sr1 sr2 = meetSearchPaths emptyBg t1 t2
+  where
+    t1 = closureOn Hypernym sr1
+    t2 = closureOn Hypernym sr2
+
+meetSearchPaths emptyBg t1 t2 =
+  let srch b v1 v2 bag1 bag2
+        | isEmptyBag bag1 && isEmptyBag bag2 = Nothing
+        | isEmptyBag bag1                    = srch (not b) v2 v1 bag2 bag1
+        | otherwise = 
+            let (Node sr chl, bag1') = splitBag bag1
+                sl = srWords sr AllSenses
+            in  if v2 `containsResult` sl
+                  then Just $ if b 
+                                then (reverse v1, sr, drop 1 $ dropWhile ((/=sl) . flip srWords AllSenses) v2) 
+                                else (reverse $ drop 1 $ dropWhile ((/=sl) . flip srWords AllSenses) v2, sr, v1)
+                  else srch (not b)
+                            v2 (addResult v1 sr) 
+                            bag2 (addListToBag bag1' chl) -- flip the order :)
+  in  srch True [] [] (addToBag emptyBg t1) (addToBag emptyBg t2)
+  where
+    containsResult v sl = sl `elem` map (flip srWords AllSenses) v
+    addResult v sr = sr:v
+
+personTree       = runWordNetQuiet (closureOn Hypernym (head $ search "person"       Noun AllSenses))
+organizationTree = runWordNetQuiet (closureOn Hypernym (head $ search "organization" Noun AllSenses))
+
diff --git a/NLP/WordNet/Common.hs b/NLP/WordNet/Common.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/Common.hs
@@ -0,0 +1,16 @@
+module NLP.WordNet.Common
+    (
+     module Data.Char,
+     module Data.List,
+     module Data.Maybe,
+     module Control.Exception,
+     module Control.Monad,
+    )
+    where
+
+import Prelude hiding (catch)
+import Data.Char
+import Data.List
+import Data.Maybe
+import Control.Exception
+import Control.Monad
diff --git a/NLP/WordNet/Consts.hs b/NLP/WordNet/Consts.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/Consts.hs
@@ -0,0 +1,29 @@
+module NLP.WordNet.Consts where
+
+import Data.List (intersperse)
+import System.FilePath (joinPath)
+
+makePath = joinPath
+
+#if defined (UNIX)
+dictDir         = "/dict"
+defaultPath     = "/usr/local/WordNet-2.0/dict"
+defaultBin      = "/usr/local/WordNet-2.0/bin"
+
+#elif defined (PC)
+dictDir         = "\\dict"
+defaultPath	= "c:\\WordNet 2.0\\dict"
+defaultBin      = "c:\\WordNet 2.0\\bin"
+
+#elif defined (MAC)
+dictDir         = ":Database"
+defaultPath     = ":Database"
+defaultBin      = ":"
+
+#else
+-- guess unix style
+dictDir         = "/dict"
+defaultPath     = "/usr/local/WordNet-2.0/dict"
+defaultBin      = "/usr/local/WordNet-2.0/bin"
+
+#endif
diff --git a/NLP/WordNet/PrimTypes.hs b/NLP/WordNet/PrimTypes.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/PrimTypes.hs
@@ -0,0 +1,205 @@
+module NLP.WordNet.PrimTypes where
+
+import Data.Array
+import System.IO
+import Control.Exception
+import Data.Dynamic (Typeable)
+
+type Offset = Integer
+
+-- | The basic part of speech type, either a 'Noun', 'Verb', 'Adj'ective or 'Adv'erb.
+data POS = Noun | Verb | Adj | Adv
+         deriving (Eq, Ord, Show, Ix, Typeable)
+allPOS = [Noun ..]
+
+data EPOS = POS POS | Satellite | AdjSatellite | IndirectAnt | DirectAnt | UnknownEPos | Pertainym
+          deriving (Eq, Ord, Typeable)
+
+fromEPOS (POS p) = p
+fromEPOS _ = Adj
+
+allEPOS = [POS Noun ..]
+
+instance Enum POS where
+  toEnum 1 = Noun
+  toEnum 2 = Verb
+  toEnum 3 = Adj
+  toEnum 4 = Adv
+  fromEnum Noun = 1
+  fromEnum Verb = 2
+  fromEnum Adj  = 3
+  fromEnum Adv  = 4
+  enumFrom i = enumFromTo i Adv
+  enumFromThen i j = enumFromThenTo i j Adv
+
+instance Enum EPOS where
+  toEnum 1 = POS Noun
+  toEnum 2 = POS Verb
+  toEnum 3 = POS Adj
+  toEnum 4 = POS Adv
+  toEnum 5 = Satellite
+  toEnum 6 = AdjSatellite
+  fromEnum (POS Noun) = 1
+  fromEnum (POS Verb) = 2
+  fromEnum (POS Adj)  = 3
+  fromEnum (POS Adv)  = 4
+  fromEnum Satellite  = 5
+  fromEnum AdjSatellite = 6
+  enumFrom i = enumFromTo i AdjSatellite
+  enumFromThen i j = enumFromThenTo i j AdjSatellite
+
+instance Show EPOS where
+  showsPrec i (POS p) = showsPrec i p
+  showsPrec i (Satellite) = showString "Satellite"
+  showsPrec i (AdjSatellite) = showString "AdjSatellite"
+  showsPrec i (IndirectAnt) = showString "IndirectAnt"
+  showsPrec i (DirectAnt) = showString "DirectAnt"
+  showsPrec i (Pertainym) = showString "Pertainym"
+  showsPrec i (UnknownEPos) = showString "UnknownEPos"
+
+instance Ix EPOS where
+  range (i,j) = [i..j]
+  index (i,j) a = fromEnum a - fromEnum i
+  inRange (i,j) a = a `elem` [i..j]
+
+
+readEPOS :: String -> EPOS
+readEPOS "n" = POS Noun
+readEPOS "v" = POS Verb
+readEPOS "a" = POS Adj
+readEPOS "r" = POS Adv
+readEPOS "s" = Satellite
+
+data WordNetEnv =
+     WordNetEnv {
+       dataHandles :: Array POS (Handle, Handle), -- index, real
+       excHandles  :: Array POS Handle, -- for morphology
+       senseHandle, 
+       countListHandle,
+       keyIndexHandle,
+       revKeyIndexHandle :: Maybe Handle,  -- these are all optional
+       vSentHandle :: Maybe (Handle, Handle), -- index, real
+       wnReleaseVersion :: Maybe String,
+       dataDirectory :: FilePath,
+       warnAbout :: String -> SomeException -> IO ()
+     }
+
+wordNetEnv0 = WordNetEnv { 
+                dataHandles = undefined,
+                excHandles  = undefined,
+                senseHandle = Nothing, 
+                countListHandle = Nothing,
+                keyIndexHandle = Nothing,
+                revKeyIndexHandle = Nothing,
+                vSentHandle = Nothing,
+                wnReleaseVersion = Nothing,
+                dataDirectory = "",
+                warnAbout = \_ _ -> return ()
+              }
+
+data SenseKey = 
+     SenseKey {
+       senseKeyPOS :: POS, 
+       senseKeyString :: String,
+       senseKeyWord :: String
+     } deriving (Show, Typeable)
+
+
+-- | A 'Key' is a simple pointer into the database, which can be
+-- followed using 'lookupKey'.
+newtype Key = Key (Offset, POS) deriving (Eq, Typeable)
+
+data Synset =
+     Synset {
+       hereIAm :: Offset,
+       ssType :: EPOS,
+       fnum :: Int,
+       pos :: EPOS,
+       ssWords :: [(String, Int, SenseType)], -- (word, lex-id, sense)
+       whichWord :: Maybe Int,
+       forms :: [(Form, Offset, EPOS, Int, Int)],
+       frames :: [(Int, Int)],
+       defn :: String,
+       key :: Maybe Offset,
+
+       searchType :: Int,
+       headWord :: String,
+       headSense :: SenseType
+     } -- deriving (Show)
+
+synset0 = Synset 0 UnknownEPos (-1) undefined [] Nothing [] [] "" Nothing (-1) "" AllSenses
+
+-- | The basic type which holds search results.  Its 'Show' instance simply
+-- shows the string corresponding to the associated WordNet synset.
+data SearchResult =
+     SearchResult {
+       srSenseKey  :: Maybe SenseKey,
+       -- | This provides (maybe) the associated overview for a SearchResult.
+       -- The 'Overview' is only available if this 'SearchResult' was
+       -- derived from a real search, rather than 'lookupKey'.
+       srOverview  :: Maybe Overview,
+       srIndex     :: Maybe Index,
+       -- | This provides (maybe) the associated sense number for a SearchResult.
+       -- The 'SenseType' is only available if this 'SearchResult' was
+       -- derived from a real search, rather than 'lookupKey'.
+       srSenseNum  :: Maybe SenseType,
+       srSynset    :: Synset
+     } deriving (Typeable)
+
+data Index = 
+     Index {
+       indexWord :: String,
+       indexPOS :: EPOS,
+       indexSenseCount :: Int,
+       indexForms :: [Form],
+       indexTaggedCount :: Int,
+       indexOffsets :: [Offset]
+     } deriving (Eq, Ord, Show, Typeable)
+
+index0 = Index "" (POS Noun) (-1) [] (-1) []
+
+-- | The different types of relations which can hold between WordNet Synsets.
+data Form = Antonym | Hypernym | Hyponym | Entailment | Similar
+          | IsMember | IsStuff | IsPart
+          | HasMember | HasStuff | HasPart
+          | Meronym | Holonym | CauseTo | PPL | SeeAlso
+          | Attribute | VerbGroup | Derivation | Classification | Class | Nominalization
+          -- misc:
+          | Syns | Freq | Frames | Coords | Relatives | HMeronym | HHolonym | WNGrep | OverviewForm
+          | Unknown
+          deriving (Eq, Ord, Show, Enum, Typeable)
+
+
+-- | A 'SenseType' is a way of controlling search.  Either you specify
+-- a certain sense (using @SenseNumber n@, or, since 'SenseType' is an
+-- instance of 'Num', you can juse use @n@) or by searching using all
+-- senses, through 'AllSenses'.  The 'Num' instance performs standard
+-- arithmetic on 'SenseNumber's, and 'fromInteger' yields a 'SenseNumber' (always),
+-- but any arithmetic involving 'AllSenses' returns 'AllSenses'.
+data SenseType = AllSenses | SenseNumber Int deriving (Eq, Ord, Show, Typeable)
+
+instance Num SenseType where
+  fromInteger = SenseNumber . fromInteger
+  SenseNumber n + SenseNumber m = SenseNumber $ n+m
+  _ + _ = AllSenses
+  SenseNumber n - SenseNumber m = SenseNumber $ n-m
+  _ - _ = AllSenses
+  SenseNumber n * SenseNumber m = SenseNumber $ n*m
+  _ * _ = AllSenses
+  negate (SenseNumber n) = SenseNumber $ negate n
+  negate x = x
+  abs (SenseNumber n) = SenseNumber $ abs n
+  abs x = x
+  signum (SenseNumber n) = SenseNumber $ signum n
+  signum x = x
+  
+
+-- | The 'Overview' type is the return type which gives you an
+-- overview of a word, for all sense and for all parts of speech.
+data Overview =
+     Overview {
+       nounIndex,
+       verbIndex,
+       adjIndex,
+       advIndex :: Maybe Index
+     } deriving (Eq, Ord, Show, Typeable)
diff --git a/NLP/WordNet/Prims.hs b/NLP/WordNet/Prims.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/Prims.hs
@@ -0,0 +1,309 @@
+-- | NLP.WordNet.Prims provides primitive operations over the word net database.
+-- The general scheme of things is to call 'initializeWordNet' to get a 'WordNetEnv'.
+-- Once you have this, you can start querying.  A query usually looks like (suppose
+-- we want "Dog" as a Noun:
+--
+-- 'getIndexString' on "Dog".  This will give us back a cannonicalized string, in this
+-- case, still "dog".  We then use 'indexLookup' to get back an index for this string.
+-- Then, we call 'indexToSenseKey' to with the index and the sense number (the Index
+-- contains the number of senses) to get back a SenseKey.  We finally call
+-- 'getSynsetForSense' on the sense key to get back a Synset.
+--
+-- We can continue to query like this or we can use the offsets provided in the
+-- various fields of the Synset to query directly on an offset.  Given an offset
+-- and a part of speech, we can use 'readSynset' directly to get a synset (instead
+-- of going through all this business with indices and sensekeys.
+module NLP.WordNet.Prims
+    (
+     initializeWordNet,
+     initializeWordNetWithOptions,
+     closeWordNet,
+     getIndexString,
+     getSynsetForSense,
+     readSynset,
+     indexToSenseKey,
+     indexLookup
+    )
+    where
+
+import System.IO -- hiding (try, catch)
+import System.Environment
+import Numeric (readHex, readDec)
+import Data.Char (toLower, isSpace)
+import Data.Array
+import Control.Exception
+import Control.Monad (when, liftM, mplus)
+import Data.List (findIndex, find)
+import Data.Maybe (isNothing, fromJust, isJust, fromMaybe)
+import GHC.IO.Handle -- (openFileEx, BinaryMode(..))
+
+import NLP.WordNet.PrimTypes
+import NLP.WordNet.Util
+import NLP.WordNet.Consts
+
+-- | initializeWordNet looks for the word net data files in the
+-- default directories, starting with environment variables WNSEARCHDIR
+-- and WNHOME, and then falling back to 'defaultPath' as defined in
+-- NLP.WordNet.Consts.
+initializeWordNet :: IO WordNetEnv
+initializeWordNet =
+  initializeWordNetWithOptions Nothing Nothing
+
+
+-- | initializeWordNetWithOptions looks for the word net data files in the
+-- specified directory.  Use this if wordnet is installed in a non-standard
+-- place on your machine and you don't have the appropriate env vars set up.
+initializeWordNetWithOptions :: 
+    Maybe FilePath ->                          -- word net data directory
+    Maybe (String -> SomeException -> IO ()) ->    -- "warning" function (by default, warnings go to stderr)
+      IO WordNetEnv
+initializeWordNetWithOptions mSearchdir mWarn = do
+  searchdir <- case mSearchdir of { Nothing -> getDefaultSearchDir ; Just d -> return d }
+  let warn = fromMaybe (\s e -> hPutStrLn stderr (s ++ "\n" ++ show e)) mWarn
+  version <- tryMaybe $ getEnv "WNDBVERSION"
+  dHands <- mapM (\pos -> do
+                  idxH  <- openFileEx
+                             (makePath [searchdir, "index." ++ partName pos])
+                             (BinaryMode ReadMode)
+                  dataH <- openFileEx 
+                             (makePath [searchdir, "data." ++ partName pos])
+                             (BinaryMode ReadMode)
+                  return (idxH, dataH)
+                 ) allPOS
+  -- the following are unnecessary
+  sense   <- tryMaybeWarn (warn "Warning: initializeWordNet: cannot open file index.sense")
+                        $ openFileEx (makePath [searchdir, "index.sense"]) (BinaryMode ReadMode)
+  cntlst  <- tryMaybeWarn (warn "Warning: initializeWordNet: cannot open file cntlist.rev")
+                        $ openFileEx (makePath [searchdir, "cntlist.rev"]) (BinaryMode ReadMode)
+  keyidx  <- tryMaybeWarn (warn "Warning: initializeWordNet: cannot open file index.key")
+                        $ openFileEx (makePath [searchdir, "index.key"  ]) (BinaryMode ReadMode)
+  rkeyidx <- tryMaybeWarn (warn "Warning: initializeWordNet: cannot open file index.key.rev")
+                        $ openFileEx (makePath [searchdir, "index.key.rev"]) (BinaryMode ReadMode)
+  vsent   <- tryMaybeWarn (warn "Warning: initializeWordNet: cannot open sentence files (sentidx.vrb and sents.vrb)")
+                        $ do
+               idx <- openFileEx (makePath [searchdir, "sentidx.vrb"]) (BinaryMode ReadMode)
+               snt <- openFileEx (makePath [searchdir, "sents.vrb"  ]) (BinaryMode ReadMode)
+               return (idx, snt)
+  mHands <- mapM (\pos -> openFileEx 
+                            (makePath [searchdir, partName pos ++ ".exc"])
+                            (BinaryMode ReadMode)) allPOS
+  return $ WordNetEnv 
+              { dataHandles = listArray (Noun, Adv) dHands,
+                excHandles  = listArray (Noun, Adv) mHands,
+                senseHandle = sense,
+                countListHandle = cntlst,
+                keyIndexHandle = keyidx,
+                revKeyIndexHandle = rkeyidx,
+                vSentHandle = vsent,
+                wnReleaseVersion = version,
+                dataDirectory = searchdir,
+                warnAbout = warn
+              }
+  where
+    getDefaultSearchDir = do
+      Just searchdir <- tryMaybe (getEnv "WNSEARCHDIR") >>= \m1 ->
+                        tryMaybe (getEnv "WNHOME") >>= \m2 ->
+                        return (m1 `mplus` 
+                                liftM (++dictDir) m2 `mplus` 
+                                Just defaultPath)
+      return searchdir
+
+-- | closeWordNet is not strictly necessary.  However, a 'WordNetEnv' tends to
+-- hog a few Handles, so if you run out of Handles and won't be using
+-- your WordNetEnv for a while, you can close it and always create a new
+-- one later.
+closeWordNet :: WordNetEnv -> IO ()
+closeWordNet wne = do
+  mapM_ (\ (h1,h2) -> hClose h1 >> hClose h2) (elems (dataHandles wne))
+  mapM_ hClose (elems (excHandles wne))
+  mapM_ (\x -> when (isJust x) $ hClose (fromJust x))
+        [senseHandle wne, countListHandle wne, keyIndexHandle wne,
+         revKeyIndexHandle wne, liftM fst (vSentHandle wne), liftM snd (vSentHandle wne)]
+
+-- | getIndexString takes a string and a part of speech and tries to find
+-- that string (or something like it) in the database.  It is essentially
+-- a cannonicalization routine and should be used before querying the
+-- database, to ensure that your string is in the right form.
+getIndexString :: WordNetEnv -> String -> POS -> IO (Maybe String)
+getIndexString wne str partOfSpeech = getIndexString' . cannonWNString $ str
+  where
+    getIndexString' [] = return Nothing
+    getIndexString' (s:ss) = do
+      i <- binarySearch (fst (dataHandles wne ! partOfSpeech)) s
+      if isJust i
+        then return (Just s)
+        else getIndexString' ss
+
+-- | getSynsetForSense takes a sensekey and finds the appropriate Synset.  SenseKeys can
+-- be built using indexToSenseKey.
+getSynsetForSense :: WordNetEnv -> SenseKey -> IO (Maybe Synset)
+getSynsetForSense wne _ | isNothing (senseHandle wne) = ioError $ userError "no sense dictionary"
+getSynsetForSense wne key = do
+  l <- binarySearch
+         (fromJust $ senseHandle wne)
+         (senseKeyString key) -- ++ " " ++ charForPOS (senseKeyPOS key))
+  case l of
+    Nothing -> return Nothing
+    Just l  -> do offset <- maybeRead $ takeWhile (not . isSpace) $
+                               drop 1 $ dropWhile (not . isSpace) l
+                  ss <- readSynset wne (senseKeyPOS key) offset (senseKeyWord key)
+                  return (Just ss)
+
+-- | readSynset takes a part of speech, and an offset (the offset can be found
+-- in another Synset) and (perhaps) a word we're looking for (this is optional)
+-- and will return its Synset.
+readSynset :: WordNetEnv -> POS -> Offset -> String -> IO Synset
+readSynset wne searchPos offset w = do
+  let h = snd (dataHandles wne ! searchPos)
+  hSeek h AbsoluteSeek offset
+  toks <- liftM words $ hGetLine h
+  --print toks
+  (ptrTokS:fnumS:posS:numWordsS:rest1) <- matchN 4 toks
+  hiam <- maybeRead ptrTokS
+  fn   <- maybeRead fnumS
+  let ss1 = synset0 { hereIAm = hiam,
+                      pos = readEPOS posS,
+                      fnum = fn,
+                      ssType = if readEPOS posS == Satellite then IndirectAnt else UnknownEPos
+                   }
+  let numWords = case readHex numWordsS of
+                   (n,_):_ -> n
+                   _       -> 0
+--read numWordsS
+  let (wrds,ptrCountS:rest2) = splitAt (numWords*2) rest1  -- words and lexids
+  let ptrCount = 
+        case readDec ptrCountS of
+          (n,_):_ -> n
+          _       -> 0
+--  print (toks, ptrCountS, ptrCount)
+  wrds' <- readWords ss1 wrds
+  let ss2 = ss1 { ssWords = wrds',
+                  whichWord = findIndex (==w) wrds }
+  let (ptrs,rest3) = splitAt (ptrCount*4) rest2
+  let (fp,ss3) = readPtrs (False,ss2) ptrs
+  let ss4 = if fp && searchPos == Adj && ssType ss3 == UnknownEPos
+              then ss3 { ssType = Pertainym }
+              else ss3
+  let (ss5,rest4) = 
+        if searchPos /= Verb 
+          then (ss4, rest3) 
+          else let (fcountS:rest4) = rest3
+                   (synPtrs, rest5) = splitAt (read fcountS * 3) rest4
+               in  (ss4, rest5)
+
+  let ss6 = ss5 { defn = unwords $ drop 1 rest4 }
+
+  return ss6
+  where
+    readWords ss (w:lid:xs) = do
+      let s = map toLower $ replaceChar ' ' '_' w
+      idx  <- indexLookup wne s (fromEPOS $ pos ss)
+--      print (w,st,idx)
+      let posn = case idx of
+                   Nothing -> Nothing
+                   Just ix -> findIndex (==hereIAm ss) (indexOffsets ix)
+      rest <- readWords ss xs
+      return ((w, fst $ head $ readHex lid, maybe AllSenses SenseNumber posn) : rest)
+    readWords _ _ = return []
+    readPtrs (fp,ss) (typ:off:ppos:lexp:xs) = 
+      let (fp',ss') = readPtrs (fp,ss) xs
+          this = (getPointerType typ,
+                  read off,
+                  readEPOS ppos,
+                  fst $ head $ readHex (take 2 lexp),
+                  fst $ head $ readHex (drop 2 lexp))
+      in  if searchPos == Adj && ssType ss' == UnknownEPos
+            then if getPointerType typ == Antonym
+                   then (fp' , ss' { forms = this : forms ss',
+                                     ssType   = DirectAnt })
+                   else (True, ss' { forms = this : forms ss' })
+            else (fp', ss' { forms = this : forms ss' })
+    readPtrs (fp,ss) _ = (fp,ss)
+
+-- | indexToSenseKey takes an Index (as returned by, ex., indexLookup) and a sense
+-- number and returns a SenseKey for that sense.
+indexToSenseKey :: WordNetEnv -> Index -> Int -> IO (Maybe SenseKey)
+indexToSenseKey wne idx sense = do
+  let cpos = fromEPOS $ indexPOS idx
+  ss1 <- readSynset wne cpos (indexOffsets idx !! (sense-1)) ""
+  ss2 <- followSatellites ss1
+  --print ss2
+  case findIndex ((==indexWord idx) . map toLower) (map (\ (w,_,_) -> w) $ ssWords ss2) of
+    Nothing -> return Nothing
+    Just  j -> do
+      let skey = if ssType ss2 == Satellite
+                   then indexWord idx ++ "%" ++ show (fromEnum Satellite) ++ ":" ++
+                        padTo 2 (show $ fnum ss2) ++ ":" ++ headWord ss2 ++ ":" ++
+                        padTo 2 (show $ headSense ss2)
+                   else indexWord idx ++ "%" ++ show (fromEnum $ pos ss2) ++ ":" ++
+                        padTo 2 (show $ fnum ss2) ++ ":" ++ 
+                        padTo 2 (show $ lexId ss2 j) ++ "::"
+      return (Just $ SenseKey cpos skey (indexWord idx))
+  where
+    followSatellites ss 
+        | ssType ss == Satellite =
+            case find (\ (f,_,_,_,_) -> f == Similar) (forms ss) of
+              Nothing -> return ss
+              Just (f,offset,p,j,k) -> do
+                adjss <- readSynset wne (fromEPOS p) offset ""
+                case ssWords adjss of
+                  (hw,_,hs):_ -> return (ss { headWord  = map toLower hw,
+                                              headSense = hs })
+                  _ -> return ss
+        | otherwise = return ss
+
+-- indexLookup takes a word and part of speech and gives back its index.
+indexLookup :: WordNetEnv -> String -> POS -> IO (Maybe Index)
+indexLookup wne w pos = do
+  ml <- binarySearch (fst (dataHandles wne ! pos)) w
+  case ml of
+    Nothing -> return Nothing
+    Just  l -> do
+      (wdS:posS:ccntS:pcntS:rest1) <- matchN 4 (words l)
+      isc <- maybeRead ccntS
+      pc  <- maybeRead pcntS
+      let idx1 = index0 { indexWord = wdS,
+                          indexPOS  = readEPOS posS,
+                          indexSenseCount = isc
+                        }
+      let (ptrs,rest2) = splitAt pc rest1
+      let idx2 = idx1 { indexForms = map getPointerType ptrs }
+      (ocntS:tcntS:rest3) <- matchN 2 rest2
+      itc <- maybeRead tcntS
+      otc <- maybeRead ocntS
+      let idx3 = idx2 { indexTaggedCount = itc }
+      let (offsets,_) = splitAt otc rest3
+      io <- mapM maybeRead offsets
+      return (Just $ idx3 { indexOffsets = io })
+
+-- do binary search on an index file
+binarySearch :: Handle -> String -> IO (Maybe String)
+binarySearch h s = do
+  hSeek h SeekFromEnd 0
+  bot <- hTell h
+  binarySearch' 0 bot (bot `div` 2)
+  where
+    binarySearch' :: Integer -> Integer -> Integer -> IO (Maybe String)
+    binarySearch' top bot mid = do
+      hSeek h AbsoluteSeek (mid-1)
+      when (mid /= 1) readUntilNL
+      eof <- hIsEOF h
+      if eof 
+        then if top >= bot-1 
+               then return Nothing
+               else binarySearch' top (bot-1) ((top+bot-1) `div` 2)
+        else do
+          l <- hGetLine h
+          let key = takeWhile (/=' ') l
+          if key == s 
+            then return (Just l)
+            else case (bot - top) `div` 2 of
+                   0 -> return Nothing
+                   d -> case key `compare` s of
+                          LT -> binarySearch' mid bot (mid + d)
+                          GT -> binarySearch' top mid (top + d)
+    readUntilNL = do
+      eof <- hIsEOF h
+      if eof 
+        then return ()
+        else do hGetLine h; return ()
diff --git a/NLP/WordNet/Types.hs b/NLP/WordNet/Types.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/Types.hs
@@ -0,0 +1,191 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  NLP.WordNet.Types
+-- Copyright   :  (c) Hal Daume III 2003-2004
+-- License     :  BSD-style
+--
+-- Maintainer  :  hdaume@isi.edu
+-- Stability   :  experimental
+-- Portability :  non-portable (H98 + implicit parameters)
+--
+-- This module is maintained at:
+--    <http://www.isi.edu/~hdaume/HWordNet/>.
+--
+-- This module describes the types used in the purely functional interface.
+--
+-- More information about WordNet is available at:
+--    <http://http://www.cogsci.princeton.edu/~wn/>.
+-------------------------------------------------------------------------------
+module NLP.WordNet.Types
+    (
+     -- * The wrapper type for wordnet functions.
+     WN(),
+
+     -- * The basic Word type (just a 'String').
+     Word,
+
+     -- * The part of speech type.
+     POS(..),
+
+     -- * The type, and functions dealing with overview searches.
+     Overview(),
+       numNounSenses,
+       numVerbSenses,
+       numAdjSenses,
+       numAdvSenses,
+       taggedCountNounSenses, 
+       taggedCountVerbSenses, 
+       taggedCountAdjSenses, 
+       taggedCountAdvSenses,
+
+     -- * The type, and functions dealing with the word net environment.
+     WordNetEnv(),
+       getReleaseVersion,
+       getDataDirectory,
+
+     -- The type, and functions dealing with senses.
+--     SenseKey(),
+--       senseKeyPOS,
+--       senseKeyWord,
+
+     -- * The type to control which sense a search is looking at.
+     SenseType(..),
+
+     -- * The type, and functions dealing with search results.
+     SearchResult(),
+       srOverview,
+       srSenseNum,
+       srPOS,
+       srDefinition,
+       srSenses,
+       srWords,
+       srForms,
+       srFormKeys,
+       srToKey,
+
+     -- The type, and functions dealing with indices.
+--     Index(),
+--       indexWord,
+--       indexPOS,
+
+     -- * A sum type of the different relations which can hold between words.
+     Form(..),
+
+     -- * A simple key into the database.
+     Key(),
+    )
+    where
+
+import System.IO.Unsafe
+import Data.List
+import Data.Maybe
+
+import NLP.WordNet.PrimTypes
+import NLP.WordNet.Util
+
+-- | In actuality this type is:
+--
+-- > type WN a = (?wne :: WordNetEnv) => a
+-- 
+-- but Haddock cannot parse this at this time.
+-- type WN a = a
+type WN a = (?wne :: WordNetEnv) => a
+
+-- | A Word is just a String.
+type Word = String
+
+-- basically, all we do is wrap the SenseKey, Synset and WordNetEnv
+-- datatypes in a bunch of useful functions
+
+-- | This will give you the current release of the WordNet databases
+-- we are using (if we know).
+getReleaseVersion :: WN (Maybe String)
+getReleaseVersion = wnReleaseVersion ?wne
+
+-- | This will give you the directory from which the databases are being read.
+getDataDirectory :: WN FilePath
+getDataDirectory = dataDirectory ?wne
+
+
+-- | Given an 'Overview', this will tell you how many noun senses the searched-for word has.
+numNounSenses :: Overview -> Int
+numNounSenses = maybe 0 indexSenseCount . nounIndex
+
+-- | Given an 'Overview', this will tell you how many verb senses the searched-for word has.
+numVerbSenses :: Overview -> Int
+numVerbSenses = maybe 0 indexSenseCount . verbIndex
+
+-- | Given an 'Overview', this will tell you how many adjective senses the searched-for word has.
+numAdjSenses :: Overview -> Int
+numAdjSenses  = maybe 0 indexSenseCount . adjIndex
+
+-- | Given an 'Overview', this will tell you how many adverb senses the searched-for word has.
+numAdvSenses :: Overview -> Int
+numAdvSenses  = maybe 0 indexSenseCount . advIndex
+
+
+-- | Given an 'Overview', this will tell you how many times this word was tagged as a noun.
+taggedCountNounSenses :: Overview -> Int
+taggedCountNounSenses = maybe 0 indexTaggedCount . nounIndex
+
+-- | Given an 'Overview', this will tell you how many times this word was tagged as a verb.
+taggedCountVerbSenses :: Overview -> Int
+taggedCountVerbSenses = maybe 0 indexTaggedCount . verbIndex
+
+-- | Given an 'Overview', this will tell you how many times this word was tagged as an adjective.
+taggedCountAdjSenses :: Overview -> Int
+taggedCountAdjSenses  = maybe 0 indexTaggedCount . adjIndex
+
+-- | Given an 'Overview', this will tell you how many times this word was tagged as an adverb.
+taggedCountAdvSenses :: Overview -> Int
+taggedCountAdvSenses  = maybe 0 indexTaggedCount . advIndex
+
+-- | This gives the part of speech of a 'SearchResult'
+srPOS :: SearchResult -> POS
+srPOS sr = case pos (srSynset sr) of { POS p -> p; _ -> Adj }
+
+-- | This gives the definition of the sense of a word in a 'SearchResult'.
+srDefinition :: SearchResult -> String
+srDefinition = defn . srSynset
+
+-- | This gives a list of senses the word has.
+srSenses :: SearchResult -> [SenseType]
+srSenses = nub . map thr3 . ssWords . srSynset
+
+-- | This gives the actual words used to describe the Synset of a search result.
+srWords :: SearchResult -> SenseType -> [Word]
+srWords sr t = nub . map fst3 . filter ((isType t) . thr3) . ssWords . srSynset $ sr
+  where isType AllSenses _ = True
+        isType _ AllSenses = True
+        isType (SenseNumber n) (SenseNumber m) = n == m
+
+-- | This gives all the 'Form's a word has (i.e., what sort of relations hold between
+-- it and other words.
+srForms :: SearchResult -> [Form]
+srForms = nub . map (\ (f,_,_,_,_) -> f) . forms . srSynset
+
+-- | This provides a 'Key' (which can be searched for using 'lookupKey') for
+-- a 'SearchResult' under a given form.  For instance, it can be used to
+-- get all 'Hypernym's of a given word.
+srFormKeys :: SearchResult -> Form -> [Key]
+srFormKeys sr f = nub . mapMaybe mkKey . filter (\ (f',_,_,_,_) -> f == f') . forms . srSynset $ sr
+  where mkKey (_,o,POS p,_,_) = Just $ Key (o,p)
+        mkKey (_,o,_,_,_)     =
+          case srSenseKey sr of
+            Nothing -> Nothing
+            Just sk -> Just $ Key (o, senseKeyPOS sk)
+
+-- | This converts a 'SearchResult' into a 'Key'.
+srToKey :: SearchResult -> Key  -- returns the key associated with this search result
+srToKey sr = Key (hereIAm $ srSynset sr, p)
+  where p = case srSenseKey sr of
+              Just sk -> senseKeyPOS sk
+              Nothing -> case pos (srSynset sr) of
+                           POS pp -> pp
+                           _ -> case ssType (srSynset sr) of
+                                  POS pp -> pp
+                                  _      -> Adj
+
+instance Show SearchResult where
+  showsPrec i (SearchResult { srSynset = ss }) =
+    showChar '<' . showString (unwords $ map fst3 $ ssWords ss) . showChar '>'
diff --git a/NLP/WordNet/Util.hs b/NLP/WordNet/Util.hs
new file mode 100644
--- /dev/null
+++ b/NLP/WordNet/Util.hs
@@ -0,0 +1,104 @@
+module NLP.WordNet.Util where
+
+import NLP.WordNet.PrimTypes
+
+import Prelude hiding (catch)
+import Control.Exception
+import Data.Char (toLower)
+import Data.List (nub)
+import Data.Maybe (fromMaybe)
+import GHC.IO.Handle
+import System.IO
+
+data IOModeEx = BinaryMode IOMode | AsciiMode IOMode deriving (Eq, Ord, Show, Read)
+
+openFileEx fp (BinaryMode md) = openBinaryFile fp md
+openFileEx fp (AsciiMode  md) = openFile fp md
+
+
+fst3 (a,_,_) = a
+snd3 (_,b,_) = b
+thr3 (_,_,c) = c
+
+maybeRead :: (Read a, Monad m) => String -> m a
+maybeRead s = 
+  case readsPrec 0 s of
+    (a,_):_ -> return a
+    _       -> fail "error parsing string"
+
+matchN :: Monad m => Int -> [a] -> m [a]
+matchN n l | length l >= n = return l
+           | otherwise     = fail "expecting more tokens"
+
+lexId x n = (\ (_,i,_) -> i) $ (ssWords x !! n)
+padTo n s = reverse $ take n $ (reverse s ++ repeat '0')
+      
+sensesOf :: Int {- num senses -} -> SenseType -> [Int]
+sensesOf n AllSenses = [1..n]
+sensesOf n (SenseNumber i)
+    | i <= 0 = []
+    | i >  n = []
+    | otherwise = [i]
+
+-- utility functions
+
+charForPOS (Noun) = "n"
+charForPOS (Verb) = "v"
+charForPOS (Adj)  = "a"
+charForPOS (Adv)  = "r"
+
+tryMaybe :: IO a -> IO (Maybe a)
+tryMaybe a = (a >>= return . Just) `catch` (\(_ :: SomeException) -> return Nothing)
+
+tryMaybeWarn :: Exception e => (e -> IO ()) -> IO a -> IO (Maybe a)
+tryMaybeWarn warn a = (a >>= return . Just) `catch` (\e -> warn e >> return Nothing)
+
+partName :: POS -> String
+partName = map toLower . show
+
+cannonWNString :: String -> [String]
+cannonWNString s'
+    | not ('_' `elem` s) &&
+      not ('-' `elem` s) &&
+      not ('.' `elem` s) = [s]
+    | otherwise = 
+        nub [s, 
+             replaceChar '_' '-' s,
+             replaceChar '-' '_' s,
+             filter (not . (`elem` "_-")) s,
+             filter (/='.') s
+            ]
+  where s = map toLower s'
+
+replaceChar from to [] = []
+replaceChar from to (c:cs)
+    | c == from = to : replaceChar from to cs
+    | otherwise = c  : replaceChar from to cs
+
+getPointerType s = fromMaybe Unknown $ lookup s l
+  where
+    l = 
+       [("!",   Antonym),
+        ("@",   Hypernym),
+        ("~",   Hyponym),
+        ("*",   Entailment),
+        ("&",   Similar),
+        ("#m",  IsMember),
+        ("#s",  IsStuff),
+        ("#p",  IsPart),
+        ("%m",  HasMember),
+        ("%s",  HasStuff),
+        ("%p",  HasPart),
+        ("%",   Meronym),
+        ("#",   Holonym),
+        (">",   CauseTo),
+        ("<",   PPL),
+        ("^",   SeeAlso),
+--        ("\\",  Pertainym),
+        ("=",   Attribute),
+        ("$",   VerbGroup),
+        ("+",   Nominalization),
+        (";",   Classification),
+        ("-",   Class),
+        -- additional searches, but not pointers.
+        ("+",    Frames)]
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,4 @@
+#! /usr/bin/env runhaskell
+
+> import Distribution.Simple
+> main = defaultMain
diff --git a/WordNet-ghc74.cabal b/WordNet-ghc74.cabal
new file mode 100644
--- /dev/null
+++ b/WordNet-ghc74.cabal
@@ -0,0 +1,33 @@
+Name:               WordNet-ghc74
+Version:            0.1.2
+Description:
+    A pure-Haskell interface to the WordNet lexical database of English.
+    Depends on the WordNet database, but not on the WordNet source code.
+Category:           natural language processing, text
+Synopsis:           Haskell interface to the WordNet database
+License:            BSD3
+License-file:       LICENSE
+Author:             Hal Daume III
+Maintainer:         Max Rabkin <max.rabkin@gmail.com>
+Build-Depends:      base, filepath
+Build-Type:         Simple
+Cabal-Version:      >= 1.8
+
+Flag small_base
+    Description: Choose the new smaller, split-up base package.
+
+Library
+    Build-depends: base == 4.*, containers, filepath, array
+        
+    Exposed-Modules:
+        NLP.WordNet
+
+    Other-Modules:
+        NLP.WordNet.Common
+        NLP.WordNet.PrimTypes
+        NLP.WordNet.Util
+        NLP.WordNet.Consts
+        NLP.WordNet.Prims
+        NLP.WordNet.Types
+
+    Extensions:     CPP, DeriveDataTypeable, ImplicitParams, MultiParamTypeClasses, RankNTypes, ScopedTypeVariables
