diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
-### 0.4.0.1
+# 0.5
 
+#### Breaking changes
+
+- `objFaces` now contain structured faces of type `Face`. A `Face` can be:
+  * a `Triangle` ;
+  * a `Quad` ;
+  * an arbritrary `Polygon`.
+  Whatever the shape of the face, it holds several `FaceIndex` used to reference locations, normals
+  and texture coordinates.
+- Ditto for `objLines`.
+
+### 0.4.0.1
+
 - Fixed a bug in the implementation of `untilEnd`.
 
 # 0.4
@@ -24,7 +36,7 @@
 #### Breaking changes
 
 - Removed `ctxtName`. It was an old function used to implement user-defined
-  objects, but since we have `Element`, we don’t those anymore.
+  objects, but since we have `Element`, we don’t need those anymore.
 
 ### 0.1.0.2
 
diff --git a/src/Codec/Wavefront/Face.hs b/src/Codec/Wavefront/Face.hs
--- a/src/Codec/Wavefront/Face.hs
+++ b/src/Codec/Wavefront/Face.hs
@@ -11,15 +11,21 @@
 
 module Codec.Wavefront.Face where
 
--- |A face is a triplet of indices. @'Face' vi vti vni@ is a face that indexes the locations with
--- @vi@, the texture coordinates with @vti@ and the normals with @vni@. An index set to 'Nothing'
--- means /no information/. That is, if @vni == 'Nothing'@, then that 'Face' doesn’t have a normal
--- associated with.
---
--- Keep in mind that 'Face' doesn’t represent a polygonal face directly. It represents a face index,
--- which is a triplet. In theory, a polygonal face is 3 'Face's.
-data Face = Face {
+import Data.List.NonEmpty ( NonEmpty )
+-- |A face index is a triplet of indices. @'FaceIndex' vi vti vni@ is a face that indexes the
+-- locations with @vi@, the texture coordinates with @vti@ and the normals with @vni@. An index set
+-- to 'Nothing' means /no information/. That is, if @vni == 'Nothing'@, then that 'FaceIndex'
+-- doesn’t have a normal associated with.
+data FaceIndex = FaceIndex {
     faceLocIndex :: {-# UNPACK #-} !Int
   , faceTexCoordIndex :: !(Maybe Int)
   , faceNorIndex :: !(Maybe Int)
   } deriving (Eq,Show)
+
+-- |A face gathers several 'FaceIndex' to build up faces, arranged in either 'Triangle', 'Quad' or
+-- an arbritatry 'Polygon'.
+data Face
+  = Triangle FaceIndex FaceIndex FaceIndex
+  | Quad FaceIndex FaceIndex FaceIndex FaceIndex
+  | Polygon (NonEmpty FaceIndex)
+    deriving (Eq,Show)
diff --git a/src/Codec/Wavefront/Lexer.hs b/src/Codec/Wavefront/Lexer.hs
--- a/src/Codec/Wavefront/Lexer.hs
+++ b/src/Codec/Wavefront/Lexer.hs
@@ -86,7 +86,7 @@
         modify $ \ctxt -> ctxt { ctxtLines = lns `append` fmap element (fromList l) }
       TknF f -> do
         (fcs,element) <- prepareElement ctxtFaces
-        modify $ \ctxt -> ctxt { ctxtFaces = fcs `append` fmap element (fromList f) }
+        modify $ \ctxt -> ctxt { ctxtFaces = fcs `snoc` element f }
       TknG g -> modify $ \ctxt -> ctxt { ctxtCurrentGroups = g }
       TknO o -> modify $ \ctxt -> ctxt { ctxtCurrentObject = Just o }
       TknMtlLib l -> do
diff --git a/src/Codec/Wavefront/Line.hs b/src/Codec/Wavefront/Line.hs
--- a/src/Codec/Wavefront/Line.hs
+++ b/src/Codec/Wavefront/Line.hs
@@ -11,14 +11,16 @@
 
 module Codec.Wavefront.Line where
 
--- |A line is a pair of indexes. @'Line' vi vti@. @vi@ references the locations and @vti@ indexes
--- the texture coordinates. If @vti == 'Nothing'@, then that 'Line' doesn’t have texture coordinates
--- associated with.
---
--- Keep in mind that 'Line' doesn’t represent a polygonal line directly. It represents a line index,
--- which is a pair. In theory, a polygonal line is 2 'Line's.
-data Line = Line {
+-- |A line index is a pair of indices. @'LineIndex' vi vti@. @vi@ references the locations and @vti@
+-- indexes the texture coordinates. If @vti == 'Nothing'@, then that 'LineIndex' doesn’t have
+-- texture coordinates associated with.
+data LineIndex = LineIndex {
     lineLocIndex :: {-# UNPACK #-} !Int
   , lineTexCoordIndex :: !(Maybe Int)
   } deriving (Eq,Show)
 
+-- A line gathers two line indices accessible by pattern matching or 'lineIndexA' and 'lineIndexB'.
+data Line = Line {
+    lineIndexA :: LineIndex
+  , lineIndexB :: LineIndex
+  } deriving (Eq,Show)
diff --git a/src/Codec/Wavefront/Token.hs b/src/Codec/Wavefront/Token.hs
--- a/src/Codec/Wavefront/Token.hs
+++ b/src/Codec/Wavefront/Token.hs
@@ -20,6 +20,7 @@
 import Control.Applicative ( Alternative(..) )
 import Data.Attoparsec.Text as AP
 import Data.Char ( isSpace )
+import Data.List.NonEmpty ( NonEmpty(..) )
 import Data.Maybe ( catMaybes )
 import Data.Text ( Text, unpack )
 import qualified Data.Text as T ( empty )
@@ -34,9 +35,9 @@
   | TknVT TexCoord
   | TknP [Point]
   | TknL [Line]
-  | TknF [Face]
+  | TknF Face
   | TknG [Text]
-  | TknO Text 
+  | TknO Text
   | TknMtlLib [Text]
   | TknUseMtl Text
     deriving (Eq,Show)
@@ -54,7 +55,7 @@
       , fmap (Just . TknVT) texCoord
       , fmap (Just . TknP) points
       , fmap (Just . TknL) lines
-      , fmap (Just . TknF) faces
+      , fmap (Just . TknF) face
       , fmap (Just . TknG) groups
       , fmap (Just . TknO) object
       , fmap (Just . TknMtlLib) mtllib
@@ -117,22 +118,40 @@
 
 ----------------------------------------------------------------------------------------------------
 -- Lines -------------------------------------------------------------------------------------------
-
--- TODO: ensure we have at least 2 pairs, otherwise it should fail
 lines :: Parser [Line]
-lines = skipSpace *> string "l " *> skipHSpace *> fmap (\(i,k) -> Line i k) parseLinePair `sepBy1` skipHSpace <* eol
+lines = do
+    skipSpace
+    string "l "
+    skipHSpace
+    pointIndices <- parsePointIndices
+    points <- case pointIndices of
+      _:_:_ -> pure $ zipWith Line pointIndices (tail pointIndices)
+      _ -> fail "line doesn't have at least two points"
+    eol
+    pure points
   where
+    parsePointIndices = fmap (\(i,j) -> LineIndex i j) parseLinePair `sepBy1` skipHSpace
     parseLinePair = do
       v <- decimal
       slashThenElse (fmap (\vt -> (v, Just vt)) decimal) (pure (v,Nothing))
 
 ----------------------------------------------------------------------------------------------------
 -- Faces -------------------------------------------------------------------------------------------
-
--- TODO: ensure we have at least 3 triples, otherwise it should fail
-faces :: Parser [Face]
-faces = skipSpace *> string "f " *> skipHSpace *> fmap (\(i,k,j) -> Face i k j) parseFaceTriple `sepBy1` skipHSpace <* eol
+face :: Parser Face
+face = do
+    skipSpace
+    string "f "
+    skipHSpace
+    faceIndices <- parseFaceIndices
+    f <- case faceIndices of
+      [a,b,c] -> pure (Triangle a b c)
+      [a,b,c,d] -> pure (Quad a b c d)
+      _:_:_:_:_:_ -> pure (Polygon $ head faceIndices :| tail faceIndices)
+      _ -> fail "face doesn't have at least three points"
+    eol
+    pure f
   where
+    parseFaceIndices = fmap (\(i,k,j) -> FaceIndex i k j) parseFaceTriple `sepBy1` skipHSpace
     parseFaceTriple = do
       v <- decimal
       slashThenElse (parseVT v) (pure (v,Nothing,Nothing))
diff --git a/wavefront.cabal b/wavefront.cabal
--- a/wavefront.cabal
+++ b/wavefront.cabal
@@ -1,5 +1,5 @@
 name:                wavefront
-version:             0.4.0.1
+version:             0.5
 synopsis:            Wavefront OBJ loader
 description:         A Wavefront OBJ loader. Currently supports polygonal information. More could
                      be added if needed (like curves and surface) if people contribute. Feel free
@@ -45,6 +45,7 @@
                      , dlist        >= 0.7  && < 0.8
                      , filepath     >= 1.4  && < 1.5
                      , mtl          >= 2.2  && < 2.3
+                     , semigroups   >= 0.18 && < 0.19
                      , text         >= 1.2  && < 1.3
                      , transformers >= 0.4  && < 0.5
                      , vector       >= 0.11 && < 0.12
