diff --git a/Data/Tagset/Positional.hs b/Data/Tagset/Positional.hs
new file mode 100644
--- /dev/null
+++ b/Data/Tagset/Positional.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE PatternGuards #-}
+
+module Data.Tagset.Positional
+( Tagset (..)
+, Attr
+, POS
+, Optional
+, domain
+, rule
+
+, Tag (..)
+, expand
+, tagSim
+) where
+
+import Control.Arrow (first)
+import qualified Data.Text as T
+import qualified Data.Map as M
+import qualified Data.Set as S
+
+-- | Attribute name.
+type Attr = T.Text
+
+-- | Part of speech.
+type POS = T.Text
+
+-- | Is the attribute optional?
+type Optional = Bool
+
+-- | The tagset consists of a domain for each attribute name and of a
+-- parsing rule for each part of speech.
+data Tagset = Tagset
+    { domains   :: M.Map Attr (S.Set T.Text)
+    , rules     :: M.Map POS  [(Attr, Optional)]
+    } deriving (Show)
+
+-- | Set of potential values for the given attribute.
+domain :: Tagset -> Attr -> S.Set T.Text
+domain Tagset{..} x =
+  case x `M.lookup` domains of
+    Just y  -> y
+    Nothing -> error $ "domain: unknown attribute " ++ T.unpack x
+
+-- | Parsing rule for the given POS.
+rule :: Tagset -> POS -> [(Attr, Optional)]
+rule Tagset{..} x =
+  case x `M.lookup` rules of
+    Just y  -> y
+    Nothing -> error $ "rule: unknown POS " ++ T.unpack x
+
+-- | The morphosyntactic tag consists of the POS value and corresponding
+-- attribute values.
+data Tag = Tag
+    { pos   :: POS
+    , atts  :: M.Map Attr T.Text
+    } deriving (Show, Read, Eq, Ord)
+
+-- | Expand optional attributes of the tag.
+expand :: Tagset -> Tag -> [Tag]
+expand tagset tag = do
+    values <- sequence (map attrVal rl)
+    let attrMap = M.fromList $ zip (map fst rl) values
+    return $ Tag (pos tag) attrMap
+  where
+    rl = rule tagset (pos tag)
+    attrVal (attr, False) = [atts tag M.! attr]
+    attrVal (attr, True)
+        | Just x <- M.lookup attr (atts tag) = [x]
+        | otherwise = S.toList $ domain tagset attr
+
+-- | Measure of similarity between two tags.
+tagSim :: Tag -> Tag -> Int
+tagSim t t' =
+    S.size (xs `S.intersection` xs')
+  where
+    xs  = S.fromList $ (Nothing, pos t)  : assocs t
+    xs' = S.fromList $ (Nothing, pos t') : assocs t'
+    assocs = map (first Just) . M.assocs . atts
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2012, IPI PAN
+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.
+
+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/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/Text/Tagset/Positional.hs b/Text/Tagset/Positional.hs
new file mode 100644
--- /dev/null
+++ b/Text/Tagset/Positional.hs
@@ -0,0 +1,131 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternGuards #-}
+
+-- | Parsing and printing positional tags and tagsets.
+
+module Text.Tagset.Positional
+( parseTag
+, showTag
+, parseTagset
+) where
+
+import Control.Applicative ((<$>), (<*>), (<*), (*>))
+import Text.Parsec
+import Data.Char (isSpace)
+import Data.Maybe (catMaybes)
+import qualified Data.Set as S
+import qualified Data.Map as M
+import qualified Data.Text as T
+
+import Data.Tagset.Positional
+
+-- | Parse the tag given the corresponding tagset.
+parseTag :: Tagset -> T.Text -> Tag
+parseTag tagset inp =
+    Tag _pos . M.fromList $ parseRule (rule tagset _pos) attrVals
+  where
+    (_pos : attrVals) = T.split (==':') inp
+    parseRule ((attr, opt):restAtts) (x:xs)
+        | x `S.member` domain tagset attr
+            = (attr, x) : parseRule restAtts xs
+        | opt == True   = parseRule restAtts (x:xs)
+        | otherwise     = error $ "parseRule:"
+            ++ " no value for " ++ T.unpack attr
+            ++ " attribute in tag " ++ T.unpack inp
+    parseRule [] [] = []
+    parseRule ((_, True):restAtts) [] = parseRule restAtts []
+    parseRule _ [] = error $ "parseRule: unexpected end of input in tag "
+        ++ T.unpack inp
+    parseRule [] _ = error $ "parseRule: input too long in tag "
+        ++ T.unpack inp
+
+-- | Print the tag given the corresponding tagset.
+showTag :: Tagset -> Tag -> T.Text
+showTag tagset tag =
+    T.intercalate ":" (pos tag : catMaybes attrVals)
+  where
+    attrVals = map showAttr $ rule tagset (pos tag)
+    showAttr (attr, opt)
+        | Just x <- M.lookup attr (atts tag) = Just x
+        | opt == True = Nothing
+        | otherwise = error $
+            "showTag: no value for mandatory attribute " ++ T.unpack attr
+
+-- | Below we defined the parser for the positional tagset.
+
+type Parser = Parsec String ()
+
+tagsetFile :: Parser Tagset
+tagsetFile = spaces *> (Tagset <$> attrSec <*> ruleSec)
+
+attrSec :: Parser (M.Map Attr (S.Set T.Text))
+attrSec = do
+    secName "ATTR" *> spaces
+    defs <- attrLine `endBy` spaces
+    return $ M.fromList defs
+
+attrLine :: Parser (Attr, S.Set T.Text)
+attrLine = do
+    attr <- ident
+    _ <- spaces *> char '=' *> lineSpaces
+    values <- map T.pack <$> ident `endBy` lineSpaces
+    return (T.pack attr, S.fromList values)
+
+ruleSec :: Parser (M.Map POS [(Attr, Optional)])
+ruleSec = do
+    secName "RULE" *> spaces
+    M.fromList <$> ruleLine `endBy` spaces
+
+ruleLine :: Parser (POS, [(Attr, Optional)])
+ruleLine = do
+    _pos <- ident
+    _ <- lineSpaces *> char '=' *> lineSpaces
+    actionAtts <- attrName `endBy` lineSpaces
+    return $ (T.pack _pos, actionAtts)
+
+attrName :: Parser (Attr, Optional)
+attrName = optionalAttrName <|> plainAttrName <?> "attribute name"
+
+optionalAttrName :: Parser (Attr, Optional)
+optionalAttrName = do
+    name <- char '[' *> ident <*  char ']'
+    return (T.pack name, True)
+
+plainAttrName :: Parser (Attr, Optional)
+plainAttrName = do
+    name <- ident
+    return $ (T.pack name, False)
+
+lineSpace :: Parser Char
+lineSpace = satisfy $ \c -> (isSpace c) && (not $ c == '\n')
+
+lineSpaces :: Parser String
+lineSpaces = many lineSpace
+
+ident :: Parser String
+ident = many1 $ oneOf $ ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "."
+
+secName :: String -> Parser Char
+secName name = char '[' *> string name *> char ']'
+
+-- | Parse the textual representation of the tagset.  The first argument
+-- should be the name of the source.
+parseTagset :: String -> String -> Tagset
+parseTagset src contents = do
+    case parse tagsetFile src filtered of
+        Left e  -> error $ "parseTagset: Error parsing input:\n" ++ show e
+        Right r -> r
+  where
+     filtered = unlines $ map (removeComment '#') $ lines contents
+
+removeComment :: Char -> String -> String
+removeComment commChar s = case findComment s of
+    Just i -> fst $ splitAt i s
+    Nothing -> s
+  where
+    findComment xs = doFind xs 0 False
+    doFind (x:xs) acc inQuot
+        | x == commChar && not inQuot = Just acc
+        | x == '"' = doFind xs (acc + 1) (not inQuot)
+        | otherwise =  doFind xs (acc + 1) inQuot
+    doFind [] _ _ = Nothing
diff --git a/tagset-positional.cabal b/tagset-positional.cabal
new file mode 100644
--- /dev/null
+++ b/tagset-positional.cabal
@@ -0,0 +1,33 @@
+name:               tagset-positional
+version:            0.1.0
+synopsis:           Handling positional tags and tagsets
+description:
+    The library provides printing and parsing functions for positional
+    tags and tagsets.
+license:            BSD3
+license-file:       LICENSE
+cabal-version:      >= 1.6
+copyright:          Copyright (c) 2012 IPI PAN
+author:             Jakub Waszczuk
+maintainer:         waszczuk.kuba@gmail.com
+stability:          experimental
+category:           Natural Language Processing
+homepage:           https://github.com/kawu/positional-tagset
+build-type:         Simple
+
+library
+    build-depends:
+        base >= 4 && < 5
+      , containers
+      , parsec
+      , text
+
+    exposed-modules:
+        Data.Tagset.Positional
+      , Text.Tagset.Positional
+
+    ghc-options: -Wall
+
+source-repository head
+    type: git
+    location: git://github.com/kawu/tagset-positional.git
