maposmatic-dev
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[Maposmatic-dev] [PATCH ocitysmap 1/3] Rename OCity SMap Python module f


From: Maxime Petazzoni
Subject: [Maposmatic-dev] [PATCH ocitysmap 1/3] Rename OCity SMap Python module from ocitysmap2 to ocitysmap
Date: Mon, 7 May 2012 19:10:03 -0700

Renamed ocitysmap2-render as render.py since we don't need/want
"ocitysmap2" in the name anymore. Rename conf template as
ocitysmap.conf.dist for the same reason. Updated INSTALL instructions
accordingly.

Signed-off-by: Maxime Petazzoni <address@hidden>
---
 .gitignore                                    |    3 +-
 INSTALL                                       |   24 +-
 i18n.py                                       |    6 +-
 ocitysmap.conf.dist                           |   24 +
 ocitysmap/.gitignore                          |    1 +
 ocitysmap/__init__.py                         |  556 ++++++++++++++
 ocitysmap/coords.py                           |  203 +++++
 ocitysmap/draw_utils.py                       |  251 +++++++
 ocitysmap/i18n.py                             |  977 +++++++++++++++++++++++++
 ocitysmap/indexlib/__init__.py                |  134 ++++
 ocitysmap/indexlib/commons.py                 |  284 +++++++
 ocitysmap/indexlib/indexer.py                 |  486 ++++++++++++
 ocitysmap/indexlib/multi_page_renderer.py     |  283 +++++++
 ocitysmap/indexlib/renderer.py                |  588 +++++++++++++++
 ocitysmap/layoutlib/__init__.py               |   45 ++
 ocitysmap/layoutlib/abstract_renderer.py      |  281 +++++++
 ocitysmap/layoutlib/commons.py                |   35 +
 ocitysmap/layoutlib/multi_page_renderer.py    |  783 ++++++++++++++++++++
 ocitysmap/layoutlib/renderers.py              |   23 +
 ocitysmap/layoutlib/single_page_renderers.py  |  690 +++++++++++++++++
 ocitysmap/maplib/__init__.py                  |   23 +
 ocitysmap/maplib/grid.py                      |  167 +++++
 ocitysmap/maplib/map_canvas.py                |  229 ++++++
 ocitysmap/maplib/overview_grid.py             |   74 ++
 ocitysmap/maplib/shapes.py                    |  192 +++++
 ocitysmap2-render                             |  241 ------
 ocitysmap2.conf-template                      |   24 -
 ocitysmap2/.gitignore                         |    1 -
 ocitysmap2/__init__.py                        |  557 --------------
 ocitysmap2/coords.py                          |  203 -----
 ocitysmap2/draw_utils.py                      |  250 -------
 ocitysmap2/i18n.py                            |  977 -------------------------
 ocitysmap2/indexlib/__init__.py               |  135 ----
 ocitysmap2/indexlib/commons.py                |  282 -------
 ocitysmap2/indexlib/indexer.py                |  488 ------------
 ocitysmap2/indexlib/multi_page_renderer.py    |  281 -------
 ocitysmap2/indexlib/renderer.py               |  589 ---------------
 ocitysmap2/layoutlib/__init__.py              |   45 --
 ocitysmap2/layoutlib/abstract_renderer.py     |  282 -------
 ocitysmap2/layoutlib/commons.py               |   35 -
 ocitysmap2/layoutlib/multi_page_renderer.py   |  787 --------------------
 ocitysmap2/layoutlib/renderers.py             |   23 -
 ocitysmap2/layoutlib/single_page_renderers.py |  692 -----------------
 ocitysmap2/maplib/__init__.py                 |   23 -
 ocitysmap2/maplib/grid.py                     |  167 -----
 ocitysmap2/maplib/map_canvas.py               |  229 ------
 ocitysmap2/maplib/overview_grid.py            |   74 --
 ocitysmap2/maplib/shapes.py                   |  192 -----
 render.py                                     |  243 ++++++
 setup.py                                      |   16 +-
 support/test-suite.sh                         |   76 +-
 51 files changed, 6635 insertions(+), 6639 deletions(-)
 create mode 100644 ocitysmap.conf.dist
 create mode 100644 ocitysmap/.gitignore
 create mode 100644 ocitysmap/__init__.py
 create mode 100644 ocitysmap/coords.py
 create mode 100644 ocitysmap/draw_utils.py
 create mode 100644 ocitysmap/i18n.py
 create mode 100644 ocitysmap/indexlib/__init__.py
 create mode 100644 ocitysmap/indexlib/commons.py
 create mode 100644 ocitysmap/indexlib/indexer.py
 create mode 100644 ocitysmap/indexlib/multi_page_renderer.py
 create mode 100644 ocitysmap/indexlib/renderer.py
 create mode 100644 ocitysmap/layoutlib/__init__.py
 create mode 100644 ocitysmap/layoutlib/abstract_renderer.py
 create mode 100644 ocitysmap/layoutlib/commons.py
 create mode 100644 ocitysmap/layoutlib/multi_page_renderer.py
 create mode 100644 ocitysmap/layoutlib/renderers.py
 create mode 100644 ocitysmap/layoutlib/single_page_renderers.py
 create mode 100644 ocitysmap/maplib/__init__.py
 create mode 100644 ocitysmap/maplib/grid.py
 create mode 100644 ocitysmap/maplib/map_canvas.py
 create mode 100644 ocitysmap/maplib/overview_grid.py
 create mode 100644 ocitysmap/maplib/shapes.py
 delete mode 100755 ocitysmap2-render
 delete mode 100644 ocitysmap2.conf-template
 delete mode 100644 ocitysmap2/.gitignore
 delete mode 100644 ocitysmap2/__init__.py
 delete mode 100644 ocitysmap2/coords.py
 delete mode 100644 ocitysmap2/draw_utils.py
 delete mode 100644 ocitysmap2/i18n.py
 delete mode 100644 ocitysmap2/indexlib/__init__.py
 delete mode 100644 ocitysmap2/indexlib/commons.py
 delete mode 100644 ocitysmap2/indexlib/indexer.py
 delete mode 100644 ocitysmap2/indexlib/multi_page_renderer.py
 delete mode 100644 ocitysmap2/indexlib/renderer.py
 delete mode 100644 ocitysmap2/layoutlib/__init__.py
 delete mode 100644 ocitysmap2/layoutlib/abstract_renderer.py
 delete mode 100644 ocitysmap2/layoutlib/commons.py
 delete mode 100644 ocitysmap2/layoutlib/multi_page_renderer.py
 delete mode 100644 ocitysmap2/layoutlib/renderers.py
 delete mode 100644 ocitysmap2/layoutlib/single_page_renderers.py
 delete mode 100644 ocitysmap2/maplib/__init__.py
 delete mode 100644 ocitysmap2/maplib/grid.py
 delete mode 100644 ocitysmap2/maplib/map_canvas.py
 delete mode 100644 ocitysmap2/maplib/overview_grid.py
 delete mode 100644 ocitysmap2/maplib/shapes.py
 create mode 100755 render.py

diff --git a/.gitignore b/.gitignore
index 3191e0f..ab8aa1b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 *~
 *.mo
-/ocitysmap.conf.mine
-/support/planet-update-daily.sh
+/ocitysmap.conf
 /support/shoreline-update.sh
 /stylesheet/maposmatic-printable/inc/datasource-settings.xml.inc
 /stylesheet/maposmatic-printable/inc/fontset-settings.xml.inc
diff --git a/INSTALL b/INSTALL
index ca0b3f4..c09d5e3 100644
--- a/INSTALL
+++ b/INSTALL
@@ -238,8 +238,8 @@ are using. They have been tested on several x86_64 hosts.
     d. Configuration
 
     python ./generate_xml.py --dbname maposmatic --host 'localhost' \
-                             --user maposmatic --port 5432 \
-                             --password 'ereiamjh'
+      --user maposmatic --port 5432 \
+      --password 'ereiamjh'
 
 11. Installation of OCitySMap
 
@@ -264,7 +264,7 @@ are using. They have been tested on several x86_64 hosts.
     d. Configuration file
 
     Create a ~/.ocitysmap.conf configuration file, modeled after the
-    provided ocitysmap2.conf-template file.
+    provided ocitysmap2.conf.dist file.
 
 12. Run OCitySMap
 
@@ -290,10 +290,10 @@ Appendix A:  Installation of maposmatic-printable 
stylesheet
        cd stylesheet/maposmatic-printable/
 
        python ./generate_xml.py --dbname maposmatic --host 'localhost' \
-                             --user maposmatic --port 5432 \
-                             --password 'ereiamjh' \
-                    --world_boundaries mapnik2-osm/world_boundaries \
-                   --symbols mapnik2-osm/symbols
+         --user maposmatic --port 5432 \
+         --password 'ereiamjh' \
+         --world_boundaries mapnik2-osm/world_boundaries \
+         --symbols mapnik2-osm/symbols
 
 Appendix B: installation of the MapQuest stylesheet
 -------------------------------------------------
@@ -351,11 +351,11 @@ replace "Arial" by "DejaVu".
 Create the .inc files from the templates:
 
  python /path/to/mapnik2-osm/generate_xml.py --inc mapquest_inc \
-                                             --symbols mapquest_symbols \
-                                             --dbname maposmatic \
-                                             --host 'localhost' \
-                                             --user maposmatic --port 5432 \
-                                             --password 'ereiamjh'
+   --symbols mapquest_symbols \
+   --dbname maposmatic \
+   --host 'localhost' \
+   --user maposmatic --port 5432 \
+   --password 'ereiamjh'
 
 The final step is to integrate this new stylesheet in ocitysmap. To do
 so, edit your ~/.ocitysmap.conf file, and add a new stylesheet
diff --git a/i18n.py b/i18n.py
index a241dc7..31eec96 100755
--- a/i18n.py
+++ b/i18n.py
@@ -32,9 +32,9 @@ def make_pot():
     print "Make locale/ocitysmap.pot"
     subprocess.check_call(['xgettext', '-o', 'ocitysmap.pot', '-p', 'locale',
                            '-L', 'Python',
-                           'ocitysmap2/indexlib/indexer.py',
-                           'ocitysmap2/layoutlib/multi_page_renderer.py',
-                           'ocitysmap2/layoutlib/single_page_renderers.py'])
+                           'ocitysmap/indexlib/indexer.py',
+                           'ocitysmap/layoutlib/multi_page_renderer.py',
+                           'ocitysmap/layoutlib/single_page_renderers.py'])
     return
 
 def make_po(languages):
diff --git a/ocitysmap.conf.dist b/ocitysmap.conf.dist
new file mode 100644
index 0000000..1681be1
--- /dev/null
+++ b/ocitysmap.conf.dist
@@ -0,0 +1,24 @@
+[datasource]
+host=localhost
+user=maposmatic
+password=mysecurepasswd
+dbname=maposmatic
+# Optional database port, defaults to 5432
+# port=5432
+
+[rendering]
+# List of available stylesheets, each needs to be described by an eponymous
+# configuration section in this file.
+available_stylesheets: stylesheet_osm1, stylesheet_osm2
+
+# The default Mapnik stylesheet.
+[stylesheet_osm1]
+name: Default
+description: The default OSM style
+path: /path/to/mapnik-osm/osm.xml
+
+# Another stylesheet
+[stylesheet_osm2]
+name: AnotherOne
+description: Another OSM Stylesheet
+path: /path/to/another/osm.xml
diff --git a/ocitysmap/.gitignore b/ocitysmap/.gitignore
new file mode 100644
index 0000000..0d20b64
--- /dev/null
+++ b/ocitysmap/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/ocitysmap/__init__.py b/ocitysmap/__init__.py
new file mode 100644
index 0000000..31b6f59
--- /dev/null
+++ b/ocitysmap/__init__.py
@@ -0,0 +1,556 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""OCitySMap 2.
+
+OCitySMap is a Mapnik-based map rendering engine from OpenStreetMap.org data.
+It is architectured around the concept of Renderers, in charge of rendering the
+map and all the visual features that go along with it (scale, grid, legend,
+index, etc.) on the given paper size using a provided Mapnik stylesheet,
+according to their implemented layout.
+
+The PlainRenderer for example renders a full-page map with its grid, a title
+header and copyright notice, but without the index.
+
+How to use OCitySMap?
+---------------------
+
+The API of OCitySMap is very simple. First, you need to instanciate the main
+OCitySMap class with the path to your OCitySMap configuration file (see
+ocitysmap.conf.dist):
+
+
+    renderer = ocitysmap.OCitySMap('/path/to/your/config')
+
+The next step is to create a RenderingConfiguration, the object that
+encapsulates all the information to parametize the rendering, including the
+Mapnik stylesheet. You can retrieve the list of supported stylesheets (directly
+as Stylesheet objects) with:
+
+    styles = renderer.get_all_style_configurations()
+
+Fill in your RenderingConfiguration with the map title, the OSM ID or bounding
+box, the chosen map language, the Stylesheet object and the paper size (in
+millimeters) and simply pass it to OCitySMap's render method:
+
+    renderer.render(rendering_configuration, layout_name,
+                    output_formats, prefix)
+
+The layout name is the renderer's key name. You can get the list of all
+supported renderers with renderer.get_all_renderers(). The output_formats is a
+list of output formats. For now, the following formats are supported:
+
+    * PNG at 72dpi
+    * PDF
+    * SVG
+    * SVGZ (gzipped-SVG)
+    * PS
+
+The prefix is the filename prefix for all the rendered files. This is usually a
+path to the destination's directory, eventually followed by some unique, yet
+common prefix for the files rendered for a job.
+"""
+
+__author__ = 'The MapOSMatic developers'
+__version__ = '0.2'
+
+import cairo
+import ConfigParser
+import gzip
+import logging
+import os
+import psycopg2
+import re
+import tempfile
+import shapely
+import shapely.wkt
+import shapely.geometry
+
+import coords
+import i18n
+from indexlib.indexer import StreetIndex
+from indexlib.commons import IndexDoesNotFitError, IndexEmptyError
+from layoutlib import PAPER_SIZES, renderers
+import layoutlib.commons
+
+LOG = logging.getLogger('ocitysmap')
+
+class RenderingConfiguration:
+    """
+    The RenderingConfiguration class encapsulate all the information concerning
+    a rendering request. This data is used by the layout renderer, in
+    conjonction with its rendering mode (defined by its implementation), to
+    produce the map.
+    """
+
+    def __init__(self):
+        self.title           = None # str
+        self.osmid           = None # None / int (shading + city name)
+        self.bounding_box    = None # bbox (from osmid if None)
+        self.language        = None # str (locale)
+
+        self.stylesheet      = None # Obj Stylesheet
+
+        self.paper_width_mm  = None
+        self.paper_height_mm = None
+
+        # Setup by OCitySMap::render() from osmid and bounding_box fields:
+        self.polygon_wkt     = None # str (WKT of interest)
+
+        # Setup by OCitySMap::render() from language field:
+        self.i18n            = None # i18n object
+
+
+class Stylesheet:
+    """
+    A Stylesheet object defines how the map features will be rendered. It
+    contains information pointing to the Mapnik stylesheet and other styling
+    parameters.
+    """
+    DEFAULT_ZOOM_LEVEL = 16
+
+    def __init__(self):
+        self.name        = None # str
+        self.path        = None # str
+        self.description = '' # str
+
+        self.grid_line_color = 'black'
+        self.grid_line_alpha = 0.5
+        self.grid_line_width = 1
+
+        self.shade_color = 'black'
+        self.shade_alpha = 0.1
+
+        # shade color for town contour in multi-pages
+        self.shade_color_2 = 'white'
+        self.shade_alpha_2 = 0.4
+
+    @staticmethod
+    def create_from_config_section(parser, section_name):
+        """Creates a Stylesheet object from the OCitySMap configuration.
+
+        Args:
+            parser (ConfigParser.ConfigParser): the configuration parser
+                object.
+            section_name (string): the stylesheet section name in the
+                configuration.
+        """
+        s = Stylesheet()
+
+        def assign_if_present(key, cast_fn=str):
+            if parser.has_option(section_name, key):
+                setattr(s, key, cast_fn(parser.get(section_name, key)))
+
+        s.name = parser.get(section_name, 'name')
+        s.path = parser.get(section_name, 'path')
+        if not os.path.exists(s.path):
+            raise ValueError, \
+                'Could not find stylesheet file for stylesheet %s!' % s.name
+        assign_if_present('description')
+
+        assign_if_present('grid_line_color')
+        assign_if_present('grid_line_alpha', float)
+        assign_if_present('grid_line_width', int)
+
+        assign_if_present('shade_color')
+        assign_if_present('shade_alpha', float)
+
+        assign_if_present('shade_color_2')
+        assign_if_present('shade_alpha_2', float)
+        return s
+
+    @staticmethod
+    def create_all_from_config(parser):
+        styles = parser.get('rendering', 'available_stylesheets')
+        if not styles:
+            raise ValueError, \
+                    'OCitySMap configuration does not contain any stylesheet!'
+
+        return [Stylesheet.create_from_config_section(parser, name.strip())
+                for name in styles.split(',')]
+
+class OCitySMap:
+    """
+    This is the main entry point of the OCitySMap map rendering engine. Read
+    this module's documentation for more details on its API.
+    """
+
+    DEFAULT_REQUEST_TIMEOUT_MIN = 15
+
+    DEFAULT_RENDERING_PNG_DPI = 72
+
+    STYLESHEET_REGISTRY = []
+
+    def __init__(self, config_files=None):
+        """Instanciate a new configured OCitySMap instance.
+
+        Args:
+            config_file (string or list or None): path, or list of paths to
+                the OCitySMap configuration file(s). If None, sensible defaults
+                are tried.
+        """
+
+        if config_files is None:
+            config_files = ['/etc/ocitysmap.conf', '~/.ocitysmap.conf']
+        elif not isinstance(config_files, list):
+            config_files = [config_files]
+
+        config_files = map(os.path.expanduser, config_files)
+        LOG.debug('Reading OCitySMap configuration from %s...' %
+                 ', '.join(config_files))
+
+        self._parser = ConfigParser.RawConfigParser()
+        if not self._parser.read(config_files):
+            raise IOError, 'None of the configuration files could be read!'
+
+        self._locale_path = os.path.join(os.path.dirname(__file__), '..', 
'locale')
+        self.__db = None
+
+        # Read stylesheet configuration
+        self.STYLESHEET_REGISTRY = 
Stylesheet.create_all_from_config(self._parser)
+        LOG.debug('Found %d Mapnik stylesheets.' % 
len(self.STYLESHEET_REGISTRY))
+
+    @property
+    def _db(self):
+        if self.__db:
+            return self.__db
+
+        # Database connection
+        datasource = dict(self._parser.items('datasource'))
+        # The port is not a mandatory configuration option, so make
+        # sure we define a default value.
+        if not datasource.has_key('port'):
+            datasource['port'] = 5432
+        LOG.info('Connecting to database %s on %s:%s as %s...' %
+                 (datasource['dbname'], datasource['host'], datasource['port'],
+                  datasource['user']))
+
+        db = psycopg2.connect(user=datasource['user'],
+                              password=datasource['password'],
+                              host=datasource['host'],
+                              database=datasource['dbname'],
+                              port=datasource['port'])
+
+        # Force everything to be unicode-encoded, in case we run along Django
+        # (which loads the unicode extensions for psycopg2)
+        db.set_client_encoding('utf8')
+
+        # Make sure the DB is correctly installed
+        self._verify_db(db)
+
+        try:
+            timeout = int(self._parser.get('datasource', 'request_timeout'))
+        except (ConfigParser.NoOptionError, ValueError):
+            timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN
+        self._set_request_timeout(db, timeout)
+
+        self.__db = db
+        return self.__db
+
+    def _verify_db(self, db):
+        """Make sure the PostGIS DB is compatible with us."""
+        cursor = db.cursor()
+        cursor.execute("""
+SELECT ST_AsText(ST_LongestLine(
+                    'POINT(100 100)'::geometry,
+                   'LINESTRING(20 80, 98 190, 110 180, 50 75 )'::geometry)
+               ) As lline;
+""")
+        assert cursor.fetchall()[0][0] == "LINESTRING(100 100,98 190)", \
+            LOG.fatal("PostGIS >= 1.5 required for correct operation !")
+
+    def _set_request_timeout(self, db, timeout_minutes=15):
+        """Sets the PostgreSQL request timeout to avoid long-running queries on
+        the database."""
+        cursor = db.cursor()
+        cursor.execute('set session statement_timeout=%d;' %
+                       (timeout_minutes * 60 * 1000))
+        cursor.execute('show statement_timeout;')
+        LOG.debug('Configured statement timeout: %s.' %
+                  cursor.fetchall()[0][0])
+
+    def _cleanup_tempdir(self, tmpdir):
+        LOG.debug('Cleaning up %s...' % tmpdir)
+        for root, dirs, files in os.walk(tmpdir, topdown=False):
+            for name in files:
+                os.remove(os.path.join(root, name))
+            for name in dirs:
+                os.rmdir(os.path.join(root, name))
+        os.rmdir(tmpdir)
+
+    def _get_geographic_info(self, osmid, table):
+        """Return the area for the given osm id in the given table, or raise
+        LookupError when not found
+
+        Args:
+            osmid (integer): OSM ID
+            table (str): either 'polygon' or 'line'
+
+        Return:
+            Geos geometry object
+        """
+
+        # Ensure all OSM IDs are integers, bust cast them back to strings
+        # afterwards.
+        LOG.debug('Looking up bounding box and contour of OSM ID %d...'
+                  % osmid)
+
+        cursor = self._db.cursor()
+        cursor.execute("""select
+                            st_astext(st_transform(st_buildarea(st_union(way)),
+                                                   4002))
+                          from planet_osm_%s where osm_id = %d
+                          group by osm_id;""" %
+                       (table, osmid))
+        records = cursor.fetchall()
+        try:
+            ((wkt,),) = records
+            if wkt is None:
+                raise ValueError
+        except ValueError:
+            raise LookupError("OSM ID %d not found in table %s" %
+                              (osmid, table))
+
+        return shapely.wkt.loads(wkt)
+
+    def get_geographic_info(self, osmid):
+        """Return a tuple (WKT_envelope, WKT_buildarea) or raise
+        LookupError when not found
+
+        Args:
+            osmid (integer): OSM ID
+
+        Return:
+            tuple (WKT bbox, WKT area)
+        """
+        found = False
+
+        # Scan polygon table:
+        try:
+            polygon_geom = self._get_geographic_info(osmid, 'polygon')
+            found = True
+        except LookupError:
+            polygon_geom = shapely.geometry.Polygon()
+
+        # Scan line table:
+        try:
+            line_geom = self._get_geographic_info(osmid, 'line')
+            found = True
+        except LookupError:
+            line_geom = shapely.geometry.Polygon()
+
+        # Merge results:
+        if not found:
+            raise LookupError("No such OSM id: %d" % osmid)
+
+        result = polygon_geom.union(line_geom)
+        return (result.envelope.wkt, result.wkt)
+
+    def get_osm_database_last_update(self):
+        cursor = self._db.cursor()
+        query = "select last_update from maposmatic_admin;"
+        try:
+            cursor.execute(query)
+        except psycopg2.ProgrammingError:
+            self._db.rollback()
+            return None
+        # Extract datetime object. It is located as the first element
+        # of a tuple, itself the first element of an array.
+        return cursor.fetchall()[0][0]
+
+    def get_all_style_configurations(self):
+        """Returns the list of all available stylesheet configurations (list of
+        Stylesheet objects)."""
+        return self.STYLESHEET_REGISTRY
+
+    def get_stylesheet_by_name(self, name):
+        """Returns a stylesheet by its key name."""
+        for style in self.STYLESHEET_REGISTRY:
+            if style.name == name:
+                return style
+        raise LookupError, 'The requested stylesheet %s was not found!' % name
+
+    def get_all_renderers(self):
+        """Returns the list of all available layout renderers (list of
+        Renderer classes)."""
+        return renderers.get_renderers()
+
+    def get_all_paper_sizes(self):
+        return PAPER_SIZES
+
+    def render(self, config, renderer_name, output_formats, file_prefix):
+        """Renders a job with the given rendering configuration, using the
+        provided renderer, to the given output formats.
+
+        Args:
+            config (RenderingConfiguration): the rendering configuration
+                object.
+            renderer_name (string): the layout renderer to use for this 
rendering.
+            output_formats (list): a list of output formats to render to, from
+                the list of supported output formats (pdf, svgz, etc.).
+            file_prefix (string): filename prefix for all output files.
+        """
+
+        assert config.osmid or config.bounding_box, \
+                'At least an OSM ID or a bounding box must be provided!'
+
+        output_formats = map(lambda x: x.lower(), output_formats)
+        config.i18n = i18n.install_translation(config.language,
+                                               self._locale_path)
+
+        LOG.info('Rendering with renderer %s in language: %s (rtl: %s).' %
+                 (renderer_name, config.i18n.language_code(),
+                  config.i18n.isrtl()))
+
+        # Determine bounding box and WKT of interest
+        if config.osmid:
+            osmid_bbox, osmid_area \
+                = self.get_geographic_info(config.osmid)
+
+            # Define the bbox if not already defined
+            if not config.bounding_box:
+                config.bounding_box \
+                    = coords.BoundingBox.parse_wkt(osmid_bbox)
+
+            # Update the polygon WKT of interest
+            config.polygon_wkt = osmid_area
+        else:
+            # No OSM ID provided => use specified bbox
+            config.polygon_wkt = config.bounding_box.as_wkt()
+
+        # Make sure we have a bounding box
+        assert config.bounding_box is not None
+        assert config.polygon_wkt is not None
+
+        osm_date = self.get_osm_database_last_update()
+
+        # Create a temporary directory for all our shape files
+        tmpdir = tempfile.mkdtemp(prefix='ocitysmap')
+        try:
+            LOG.debug('Rendering in temporary directory %s' % tmpdir)
+
+            # Prepare the generic renderer
+            renderer_cls = renderers.get_renderer_class_by_name(renderer_name)
+
+            # Perform the actual rendering to the Cairo devices
+            for output_format in output_formats:
+                output_filename = '%s.%s' % (file_prefix, output_format)
+                try:
+                    self._render_one(config, tmpdir, renderer_cls,
+                                     output_format, output_filename, osm_date,
+                                     file_prefix)
+                except IndexDoesNotFitError:
+                    LOG.exception("The actual font metrics probably don't "
+                                  "match those pre-computed by the renderer's"
+                                  "constructor. Backtrace follows...")
+        finally:
+            self._cleanup_tempdir(tmpdir)
+
+    def _render_one(self, config, tmpdir, renderer_cls,
+                    output_format, output_filename, osm_date, file_prefix):
+
+        LOG.info('Rendering to %s format...' % output_format.upper())
+
+        factory = None
+        dpi = layoutlib.commons.PT_PER_INCH
+
+        if output_format == 'png':
+            try:
+                dpi = int(self._parser.get('rendering', 'png_dpi'))
+            except ConfigParser.NoOptionError:
+                dpi = OCitySMap.DEFAULT_RENDERING_PNG_DPI
+
+            # As strange as it may seem, we HAVE to use a vector
+            # device here and not a raster device such as
+            # ImageSurface. Because, for some reason, with
+            # ImageSurface, the font metrics would NOT match those
+            # pre-computed by renderer_cls.__init__() and used to
+            # layout the whole page
+            def factory(w,h):
+                w_px = int(layoutlib.commons.convert_pt_to_dots(w, dpi))
+                h_px = int(layoutlib.commons.convert_pt_to_dots(h, dpi))
+                LOG.debug("Rendering PNG into %dpx x %dpx area..."
+                          % (w_px, h_px))
+                return cairo.PDFSurface(None, w_px, h_px)
+
+        elif output_format == 'svg':
+            factory = lambda w,h: cairo.SVGSurface(output_filename, w, h)
+        elif output_format == 'svgz':
+            factory = lambda w,h: cairo.SVGSurface(
+                    gzip.GzipFile(output_filename, 'wb'), w, h)
+        elif output_format == 'pdf':
+            factory = lambda w,h: cairo.PDFSurface(output_filename, w, h)
+        elif output_format == 'ps':
+            factory = lambda w,h: cairo.PSSurface(output_filename, w, h)
+        elif output_format == 'ps.gz':
+            factory = lambda w,h: cairo.PSSurface(
+                gzip.GzipFile(output_filename, 'wb'), w, h)
+        elif output_format == 'csv':
+            # We don't render maps into CSV.
+            return
+
+        else:
+            raise ValueError, \
+                'Unsupported output format: %s!' % output_format.upper()
+
+        renderer = renderer_cls(self._db, config, tmpdir, dpi, file_prefix)
+
+        surface = factory(renderer.paper_width_pt, renderer.paper_height_pt)
+
+        renderer.render(surface, dpi, osm_date)
+
+        LOG.debug('Writing %s...' % output_filename)
+        if output_format == 'png':
+            surface.write_to_png(output_filename)
+
+        surface.finish()
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG)
+
+    o = OCitySMap([os.path.join(os.path.dirname(__file__), '..',
+                                'ocitysmap.conf.mine')])
+
+    c = RenderingConfiguration()
+    c.title = 'Chevreuse, Yvelines, Île-de-France, France, Europe, Monde'
+    c.osmid = -943886 # Chevreuse
+    # c.osmid = -7444   # Paris
+    c.language = 'fr_FR.UTF-8'
+    c.paper_width_mm = 297
+    c.paper_height_mm = 420
+    c.stylesheet = o.get_stylesheet_by_name('Default')
+
+    # c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
+    o.render(c, 'single_page_index_bottom',
+             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
+             '/tmp/mymap_index_bottom')
+
+    c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
+    o.render(c, 'single_page_index_side',
+             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
+             '/tmp/mymap_index_side')
+
+    o.render(c, 'plain',
+             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
+             '/tmp/mymap_plain')
diff --git a/ocitysmap/coords.py b/ocitysmap/coords.py
new file mode 100644
index 0000000..32c1426
--- /dev/null
+++ b/ocitysmap/coords.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import math
+
+import shapely.wkt
+
+# Importing mapnik2 raises a DeprecationWarning as of mapnik
+# commit 14700dba. As mapnik 2.1 (or git version with support for
+# placement-type="simple") is required for OCitySMap (see INSTALL),
+# instead of importing mapnik2, we import mapnik and assert it isn't
+# an old version.
+import mapnik
+assert mapnik.mapnik_version >= 200100, \
+    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
+    "for more details." % mapnik.mapnik_version_string()
+
+_MAPNIK_PROJECTION = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 " \
+                     "+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m   " \
+                     "address@hidden +no_defs +over"
+
+
+EARTH_RADIUS = 6370986 # meters
+
+
+class Point:
+    def __init__(self, lat, long_):
+        self._lat, self._long = float(lat), float(long_)
+
+    @staticmethod
+    def parse_wkt(wkt):
+        long_,lat = wkt[6:-1].split()
+        return Point(lat, long_)
+
+    def get_latlong(self):
+        return self._lat, self._long
+
+    def as_wkt(self, with_point_statement=True):
+        contents = '%f %f' % (self._long, self._lat)
+        if with_point_statement:
+            return "POINT(%s)" % contents
+        return contents
+
+    def __str__(self):
+        return 'Point(lat=%f, long_=%f)' % (self._lat, self._long)
+
+    def spheric_spherical_vector(self, other):
+        """Approx (self - other) vector converted to lat/long meters
+        wrt the given other point"""
+        delta_lat  = abs(self._lat - other._lat)
+        delta_long = abs(self._long - other._long)
+        radius_lat = EARTH_RADIUS * math.cos(math.radians(self._lat))
+        return (EARTH_RADIUS * math.radians(delta_lat),
+                radius_lat * math.radians(delta_long))
+
+
+class BoundingBox:
+    """
+    The BoundingBox class defines a geographic rectangle area specified by the
+    coordinates of its top left and bottom right corners, in latitude and
+    longitude (4002 projection).
+    """
+
+    def __init__(self, lat1, long1, lat2, long2):
+        (self._lat1, self._long1) = float(lat1), float(long1)
+        (self._lat2, self._long2) = float(lat2), float(long2)
+
+        # make sure lat1/long1 is the upper left, and the others the btm right
+        if (self._lat1 < self._lat2):
+            self._lat1, self._lat2 = self._lat2, self._lat1
+        if (self._long1 > self._long2):
+            self._long1, self._long2 = self._long2, self._long1
+
+    @staticmethod
+    def parse_wkt(wkt):
+        """Returns a BoundingBox object created from the coordinates of a
+        polygon given in WKT format."""
+        try:
+            geom_envelope = shapely.wkt.loads(wkt).bounds
+        except Exception, rx:
+            raise ValueError("Invalid input WKT: %s" % ex)
+        return BoundingBox(geom_envelope[1], geom_envelope[0],
+                           geom_envelope[3], geom_envelope[2])
+
+    @staticmethod
+    def parse_latlon_strtuple(points):
+        """Returns a BoundingBox object from a tuple of strings
+        [("lat1,lon1"), ("lat2,lon2")]"""
+        (lat1, long1) = points[0].split(',')
+        (lat2, long2) = points[1].split(',')
+        return BoundingBox(lat1, long1, lat2, long2)
+
+    def get_top_left(self):
+        return (self._lat1, self._long1)
+
+    def get_bottom_right(self):
+        return (self._lat2, self._long2)
+
+    def create_expanded(self, dlat, dlong):
+        """Return a new bbox of the same size + dlat/dlong added
+           on the top-left sides"""
+        return BoundingBox(self._lat1 + dlat, self._long1 - dlong,
+                           self._lat2 - dlat, self._long2 + dlong)
+
+    @staticmethod
+    def _ptstr(point):
+        return '%.4f,%.4f' % (point[0], point[1])
+
+    def __str__(self):
+        return 'BoundingBox(%s %s)' \
+            % (BoundingBox._ptstr(self.get_top_left()),
+               BoundingBox._ptstr(self.get_bottom_right()))
+
+    def as_wkt(self, with_polygon_statement=True):
+        xmax, ymin = self.get_top_left()
+        xmin, ymax = self.get_bottom_right()
+        s_coords = ("%f %f, %f %f, %f %f, %f %f, %f %f"
+                    % (ymin, xmin, ymin, xmax, ymax, xmax,
+                       ymax, xmin, ymin, xmin))
+        if with_polygon_statement:
+            return "POLYGON((%s))" % s_coords
+        return s_coords
+
+    def spheric_sizes(self):
+        """Metric distances at the bounding box top latitude.
+        Returns the tuple (metric_size_lat, metric_size_long)
+        """
+        delta_lat = abs(self._lat1 - self._lat2)
+        delta_long = abs(self._long1 - self._long2)
+        radius_lat = EARTH_RADIUS * math.cos(math.radians(self._lat1))
+        return (EARTH_RADIUS * math.radians(delta_lat),
+                radius_lat * math.radians(delta_long))
+
+    def get_pixel_size_for_zoom_factor(self, zoom):
+        """Return the size in pixels (tuple height,width) needed to
+        render the bounding box at the given zoom factor."""
+        delta_long = abs(self._long1 - self._long2)
+        # 2^zoom tiles (1 tile = 256 pix) for the whole earth
+        pix_x = delta_long * (2 ** (zoom + 8)) / 360
+
+        # http://en.wikipedia.org/wiki/Mercator_projection
+        yplan = lambda lat: math.log(math.tan(math.pi/4.0 +
+                                              math.radians(lat)/2.0))
+
+        # OSM maps are drawn between -85 deg and + 85, the whole amplitude
+        # is 256*2^(zoom)
+        pix_y = (yplan(self._lat1) - yplan(self._lat2)) \
+                * (2 ** (zoom + 7)) / yplan(85)
+
+        return (int(math.ceil(pix_y)), int(math.ceil(pix_x)))
+
+    def to_mercator(self):
+        envelope = mapnik.Box2d(self.get_top_left()[1],
+                                self.get_top_left()[0],
+                                self.get_bottom_right()[1],
+                                self.get_bottom_right()[0])
+        _proj = mapnik.Projection(_MAPNIK_PROJECTION)
+        bottom_left = _proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
+        top_right = _proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
+        top_left = mapnik.Coord(bottom_left.x, top_right.y)
+        bottom_right = mapnik.Coord(top_right.x, bottom_left.y)
+        return (bottom_right, bottom_left, top_left, top_right)
+
+    def as_javascript(self, name=None, color=None):
+        if name:
+            name_str = ", \"%s\"" % name
+        else:
+            name_str = ""
+
+        if color:
+            color_str = ", { color: \"%s\" }" % color
+        else:
+            color_str = ""
+
+        return 'BoundingBox(%f,%f,%f,%f%s%s)' % \
+            (self._lat1, self._long1, self._lat2, self._long2,
+             name_str, color_str)
+
+if __name__ == "__main__":
+    wkt = 'POINT(2.0333 48.7062132250362)'
+    pt = Point.parse_wkt(wkt)
+    print wkt, pt, pt.as_wkt()
diff --git a/ocitysmap/draw_utils.py b/ocitysmap/draw_utils.py
new file mode 100644
index 0000000..f1f6986
--- /dev/null
+++ b/ocitysmap/draw_utils.py
@@ -0,0 +1,251 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2012  David Decotigny
+# Copyright (C) 2012  Frédéric Lehobey
+# Copyright (C) 2012  Pierre Mauduit
+# Copyright (C) 2012  David Mentré
+# Copyright (C) 2012  Maxime Petazzoni
+# Copyright (C) 2012  Thomas Petazzoni
+# Copyright (C) 2012  Gaël Utard
+# Copyright (C) 2012  Étienne Loks
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import pango
+import pangocairo
+
+import ocitysmap.layoutlib.commons as commons
+
+def draw_text(ctx, pc, layout, fascent, fheight,
+              baseline_x, baseline_y, text, pango_alignment):
+    """Draws the given text into the provided Cairo
+    context through the Pango layout (get_width() expected to be
+    correct in order to position the text correctly) with the
+    specified pango.ALIGN_x alignment.
+
+    Args:
+        ctx (cairo.Context): cairo context to use
+        pc (pangocairo.CairoContext): pango context
+        layout (pango.Layout): pango layout to draw into (get_with() important)
+        fascent, fheight (int): current font ascent/height metrics
+        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
+        pango_alignment (enum): pango.ALIGN_ constant value
+
+    Results:
+        A 3-uple text_width, text_height (cairo units)
+    """
+    layout.set_auto_dir(False) # Make sure ALIGN_RIGHT is independent on RTL...
+    layout.set_alignment(pango_alignment)
+    layout.set_text(text)
+    width, height = [x/pango.SCALE for x in layout.get_size()]
+
+    ctx.move_to(baseline_x, baseline_y - fascent)
+    pc.show_layout(layout)
+    return width, height
+
+def draw_text_left(ctx, pc, layout, fascent, fheight,
+                    baseline_x, baseline_y, text):
+    """Draws the given text left aligned into the provided Cairo
+    context through the Pango layout (get_width() expected to be
+    correct in order to position the text correctly).
+
+    Args:
+        ctx (cairo.Context): cairo context to use
+        pc (pangocairo.CairoContext): pango context
+        layout (pango.Layout): pango layout to draw into (get_with() important)
+        fascent, fheight (int): current font ascent/height metrics
+        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
+        pango_alignment (enum): pango.ALIGN_ constant value
+
+    Results:
+        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
+    """
+    w,h = draw_text(ctx, pc, layout, fascent, fheight,
+                    baseline_x, baseline_y, text, pango.ALIGN_LEFT)
+    return baseline_x, baseline_y, baseline_x + w
+
+def draw_text_center(ctx, pc, layout, fascent, fheight,
+                     baseline_x, baseline_y, text):
+    """Draws the given text centered inside the provided Cairo
+    context through the Pango layout (get_width() expected to be
+    correct in order to position the text correctly).
+
+    Args:
+        ctx (cairo.Context): cairo context to use
+        pc (pangocairo.CairoContext): pango context
+        layout (pango.Layout): pango layout to draw into (get_with() important)
+        fascent, fheight (int): current font ascent/height metrics
+        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
+        pango_alignment (enum): pango.ALIGN_ constant value
+
+    Results:
+        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
+    """
+    txt_width, txt_height = draw_text(ctx, pc, layout, fascent, fheight,
+                                      baseline_x, baseline_y, text,
+                                      pango.ALIGN_CENTER)
+    layout_width = layout.get_width() / pango.SCALE
+    return ( baseline_x + (layout_width - txt_width) / 2.,
+             baseline_y,
+             baseline_x + (layout_width + txt_width) / 2. )
+
+def draw_text_right(ctx, pc, layout, fascent, fheight,
+                    baseline_x, baseline_y, text):
+    """Draws the given text right aligned into the provided Cairo
+    context through the Pango layout (get_width() expected to be
+    correct in order to position the text correctly).
+
+    Args:
+        ctx (cairo.Context): cairo context to use
+        pc (pangocairo.CairoContext): pango context
+        layout (pango.Layout): pango layout to draw into (get_with() important)
+        fascent, fheight (int): current font ascent/height metrics
+        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
+        pango_alignment (enum): pango.ALIGN_ constant value
+
+    Results:
+        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
+    """
+    txt_width, txt_height = draw_text(ctx, pc, layout, fascent, fheight,
+                                      baseline_x, baseline_y,
+                                      text, pango.ALIGN_RIGHT)
+    layout_width = layout.get_width() / pango.SCALE
+    return (baseline_x + layout_width - txt_width,
+            baseline_y,
+            baseline_x + layout_width)
+
+def draw_simpletext_center(ctx, text, x, y):
+    """
+    Draw the given text centered at x,y.
+
+    Args:
+       ctx (cairo.Context): The cairo context to use to draw.
+       text (str): the text to draw.
+       x,y (numbers): Location of the center (cairo units).
+    """
+    ctx.save()
+    xb, yb, tw, th, xa, ya = ctx.text_extents(text)
+    ctx.move_to(x - tw/2.0 - xb, y - yb/2.0)
+    ctx.show_text(text)
+    ctx.stroke()
+    ctx.restore()
+
+def draw_dotted_line(ctx, line_width, baseline_x, baseline_y, length):
+    ctx.set_line_width(line_width)
+    ctx.set_dash([line_width, line_width*2])
+    ctx.move_to(baseline_x, baseline_y)
+    ctx.rel_line_to(length, 0)
+    ctx.stroke()
+
+def adjust_font_size(layout, fd, constraint_x, constraint_y):
+    """
+    Grow the given font description (20% by 20%) until it fits in
+    designated area and then draw it.
+
+    Args:
+       layout (pango.Layout): The text block parameters.
+       fd (pango.FontDescriptor): The font object.
+       constraint_x/constraint_y (numbers): The area we want to
+           write into (cairo units).
+    """
+    while (layout.get_size()[0] / pango.SCALE < constraint_x and
+           layout.get_size()[1] / pango.SCALE < constraint_y):
+        fd.set_size(int(fd.get_size()*1.2))
+        layout.set_font_description(fd)
+    fd.set_size(int(fd.get_size()/1.2))
+    layout.set_font_description(fd)
+
+def draw_text_adjusted(ctx, text, x, y, width, height, max_char_number=None,
+                       text_color=(0, 0, 0, 1), align=pango.ALIGN_CENTER,
+                       width_adjust=0.7, height_adjust=0.8):
+    """
+    Draw a text adjusted to a maximum character number
+
+    Args:
+       ctx (cairo.Context): The cairo context to use to draw.
+       text (str): the text to draw.
+       x/y (numbers): The position on the canvas.
+       width/height (numbers): The area we want to
+           write into (cairo units).
+       max_char_number (number): If set a maximum character number.
+    """
+    pc = pangocairo.CairoContext(ctx)
+    layout = pc.create_layout()
+    layout.set_width(int(width_adjust * width * pango.SCALE))
+    layout.set_alignment(align)
+    fd = pango.FontDescription("Georgia Bold")
+    fd.set_size(pango.SCALE)
+    layout.set_font_description(fd)
+
+    if max_char_number:
+        # adjust size with the max character number
+        layout.set_text('0'*max_char_number)
+        adjust_font_size(layout, fd, width_adjust*width, height_adjust*height)
+
+    # set the real text
+    layout.set_text(text)
+    if not max_char_number:
+        adjust_font_size(layout, fd, width_adjust*width, height_adjust*height)
+
+    # draw
+    text_x, text_y, text_w, text_h = layout.get_extents()[1]
+    ctx.save()
+    ctx.set_source_rgba(*text_color)
+    if align == pango.ALIGN_CENTER:
+        x = x - (text_w/2.0)/pango.SCALE - int(float(text_x)/pango.SCALE)
+        y = y - (text_h/2.0)/pango.SCALE - int(float(text_y)/pango.SCALE)
+    else:
+        y = y - (text_h/2.0)/pango.SCALE - text_y/pango.SCALE
+    ctx.translate(x, y)
+
+    if align == pango.ALIGN_LEFT:
+        # Hack to workaround what appears to be a Cairo bug: without
+        # drawing a rectangle here, the translation above is not taken
+        # into account for rendering the text.
+        ctx.rectangle(0, 0, 0, 0)
+    pc.show_layout(layout)
+    ctx.restore()
+
+def render_page_number(ctx, page_number,
+                       usable_area_width_pt, usable_area_height_pt, margin_pt,
+                       transparent_background = True):
+    """
+    Render page number
+    """
+    ctx.save()
+    x_offset = 0
+    if page_number % 2:
+        x_offset += commons.convert_pt_to_dots(usable_area_width_pt)\
+                  - commons.convert_pt_to_dots(margin_pt)
+    y_offset = commons.convert_pt_to_dots(usable_area_height_pt)\
+             - commons.convert_pt_to_dots(margin_pt)
+    ctx.translate(x_offset, y_offset)
+
+    if transparent_background:
+        ctx.set_source_rgba(1, 1, 1, 0.6)
+    else:
+        ctx.set_source_rgba(0.8, 0.8, 0.8, 0.6)
+    ctx.rectangle(0, 0, commons.convert_pt_to_dots(margin_pt),
+                  commons.convert_pt_to_dots(margin_pt))
+    ctx.fill()
+
+    ctx.set_source_rgba(0, 0, 0, 1)
+    x_offset = commons.convert_pt_to_dots(margin_pt)/2
+    y_offset = commons.convert_pt_to_dots(margin_pt)/2
+    ctx.translate(x_offset, y_offset)
+    draw_simpletext_center(ctx, unicode(page_number), 0, 0)
+    ctx.restore()
+
diff --git a/ocitysmap/i18n.py b/ocitysmap/i18n.py
new file mode 100644
index 0000000..d476717
--- /dev/null
+++ b/ocitysmap/i18n.py
@@ -0,0 +1,977 @@
+# -*- coding: utf-8; mode: Python -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import gettext
+
+def _install_language(language, locale_path):
+    t = gettext.translation(domain='ocitysmap',
+                            localedir=locale_path,
+                            languages=[language],
+                            fallback=True)
+    t.install(unicode=True)
+
+class i18n:
+    """Functions needed to be implemented for a new language.
+       See i18n_fr_FR_UTF8 below for an example. """
+    def language_code(self):
+        pass
+
+    def user_readable_street(self, name):
+        pass
+
+    def first_letter_equal(self, a, b):
+        pass
+
+    def isrtl(self):
+        return False
+
+    def upper_unaccent_string(self, s):
+        return s.upper()
+
+class i18n_template_code_CODE(i18n):
+    def __init__(self, language, locale_path):
+        """Install the _() function for the chosen locale other
+           object initialisation"""
+
+        # It's important to convert to str() here because the map_language
+        # value coming from the database is Unicode, but setlocale() needs a
+        # non-unicode string as the locale name, otherwise it thinks it's a
+        # locale tuple.
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def language_code(self):
+        """returns the language code of the specific language
+           supported, e.g. fr_FR.UTF-8"""
+        return self.language
+
+    def user_readable_street(self, name):
+        """ transforms a street name into a suitable form for
+            the map index, e.g. Paris (Rue de) for French"""
+        return name
+
+    def first_letter_equal(self, a, b):
+        """returns True if the letters a and b are equal in the map index,
+           e.g. É and E are equals in French map index"""
+        return a == b
+
+    def isrtl(self):
+        return False
+
+
+class i18n_fr_generic(i18n):
+    APPELLATIONS = [ u"Accès", u"Allée", u"Allées", u"Autoroute", u"Avenue",
+                     u"Avenues", u"Barrage",
+                     u"Boulevard", u"Carrefour", u"Chaussée", u"Chemin",
+                     u"Chemin rural",
+                     u"Cheminement", u"Cale", u"Cales", u"Cavée", u"Cité",
+                     u"Clos", u"Coin", u"Côte", u"Cour", u"Cours", u"Descente",
+                     u"Degré", u"Escalier",
+                     u"Escaliers", u"Esplanade", u"Funiculaire",
+                     u"Giratoire", u"Hameau", u"Impasse", u"Jardin",
+                     u"Jardins", u"Liaison", u"Lotissement", u"Mail",
+                     u"Montée", u"Môle",
+                     u"Parc", u"Passage", u"Passerelle", u"Passerelles",
+                     u"Place", u"Placette", u"Pont", u"Promenade",
+                     u"Petite Avenue", u"Petite Rue", u"Quai",
+                     u"Rampe", u"Rang", u"Résidence", u"Rond-Point",
+                     u"Route forestière", u"Route", u"Rue", u"Ruelle",
+                     u"Square", u"Sente", u"Sentier", u"Sentiers", 
u"Terre-Plein",
+                     u"Télécabine", u"Traboule", u"Traverse", u"Tunnel",
+                     u"Venelle", u"Villa", u"Virage"
+                   ]
+    DETERMINANTS = [ u" des", u" du", u" de la", u" de l'",
+                     u" de", u" d'", u" aux", u""
+                   ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäãæ]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõœ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+    Y_ACCENT = re.compile(ur"[ÿ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        s = self.Y_ACCENT.sub("y", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_it_generic(i18n):
+    APPELLATIONS = [ u"Via", u"Viale", u"Piazza", u"Scali", u"Strada", 
u"Largo",
+                     u"Corso", u"Viale", u"Calle", u"Sottoportico",
+                    u"Sottoportego", u"Vicolo", u"Piazzetta" ]
+    DETERMINANTS = [ u" delle", u" dell'", u" dei", u" degli",
+                     u" della", u" del", u" di", u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_es_generic(i18n):
+    APPELLATIONS = [ u"Avenida", u"Avinguda", u"Calle", u"Callejón",
+            u"Calzada", u"Camino", u"Camí", u"Carrer", u"Carretera",
+            u"Glorieta", u"Parque", u"Pasaje", u"Pasarela", u"Paseo", u"Plaza",
+            u"Plaça", u"Privada", u"Puente", u"Ronda", u"Salida", u"Travesia" ]
+    DETERMINANTS = [ u" de la", u" de los", u" de las",
+                     u" dels", u" del", u" d'", u" de l'",
+                     u" de", u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        s = self.N_ACCENT.sub("n", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_ca_generic(i18n):
+
+    APPELLATIONS = [ # Catalan
+                     u"Autopista", u"Autovia", u"Avinguda",
+                     u"Baixada", u"Barranc", u"Barri", u"Barriada",
+                     u"Biblioteca", u"Carrer", u"Carreró", u"Carretera",
+                     u"Cantonada", u"Església", u"Estació", u"Hospital",
+                     u"Monestir", u"Monument", u"Museu", u"Passatge",
+                     u"Passeig", u"Plaça", u"Planta", u"Polígon",
+                     u"Pujada", u"Rambla", u"Ronda", u"Travessera",
+                     u"Travessia", u"Urbanització", u"Via",
+                     u"Avenida", u"Calle", u"Camino", u"Plaza",
+
+                     # Spanish (being distinct from Catalan)
+                     u"Acceso", u"Acequia", u"Alameda", u"Alquería",
+                     u"Andador", u"Angosta", u"Apartamentos", u"Apeadero",
+                     u"Arboleda", u"Arrabal", u"Arroyo", u"Autovía",
+                     u"Avenida", u"Bajada", u"Balneario", u"Banda",
+                     u"Barranco", u"Barranquil", u"Barrio", u"Bloque",
+                     u"Brazal", u"Bulevar", u"Calle", u"Calleja",
+                     u"Callejón", u"Callejuela", u"Callizo", u"Calzada",
+                     u"Camino", u"Camping", u"Cantera", u"Cantina",
+                     u"Cantón", u"Carrera", u"Carrero", u"Carreterín",
+                     u"Carretil", u"Carril", u"Caserío", u"Chalet",
+                     u"Cinturón", u"Circunvalación", u"Cobertizo",
+                     u"Colonia", u"Complejo", u"Conjunto", u"Convento",
+                     u"Cooperativa", u"Corral", u"Corralillo", u"Corredor",
+                     u"Cortijo", u"Costanilla", u"Costera", u"Cuadra",
+                     u"Cuesta", u"Dehesa", u"Demarcación", u"Diagonal",
+                     u"Diseminado", u"Edificio", u"Empresa", u"Entrada",
+                     u"Escalera", u"Escalinata", u"Espalda", u"Estación",
+                     u"Estrada", u"Explanada", u"Extramuros", u"Extrarradio",
+                     u"Fábrica", u"Galería", u"Glorieta", u"Gran Vía",
+                     u"Granja", u"Hipódromo", u"Jardín", u"Ladera",
+                     u"Llanura", u"Malecón", u"Mercado", u"Mirador",
+                     u"Monasterio", u"Muelle", u"Núcleo", u"Palacio",
+                     u"Pantano", u"Paraje", u"Parque", u"Particular",
+                     u"Partida", u"Pasadizo", u"Pasaje", u"Paseo",
+                     u"Paseo marítimo", u"Pasillo", u"Plaza", u"Plazoleta",
+                     u"Plazuela", u"Poblado", u"Polígono", u"Polígono 
industrial",
+                     u"Portal", u"Pórtico", u"Portillo", u"Prazuela",
+                     u"Prolongación", u"Pueblo", u"Puente", u"Puerta",
+                     u"Puerto", u"Punto kilométrico", u"Rampla",
+                     u"Residencial", u"Ribera", u"Rincón", u"Rinconada",
+                     u"Sanatorio", u"Santuario", u"Sector", u"Sendera",
+                     u"Sendero", u"Subida", u"Torrente", u"Tránsito",
+                     u"Transversal", u"Trasera", u"Travesía", u"Urbanización",
+                     u"Vecindario", u"Vereda", u"Viaducto", u"Viviendas",
+
+                     # French (being distinct from Catalan and Spanish)
+                     u"Accès", u"Allée", u"Allées", u"Autoroute", u"Avenue", 
u"Barrage",
+                     u"Boulevard", u"Carrefour", u"Chaussée", u"Chemin",
+                     u"Cheminement", u"Cale", u"Cales", u"Cavée", u"Cité",
+                     u"Clos", u"Coin", u"Côte", u"Cour", u"Cours", u"Descente",
+                     u"Degré", u"Escalier",
+                     u"Escaliers", u"Esplanade", u"Funiculaire",
+                     u"Giratoire", u"Hameau", u"Impasse", u"Jardin",
+                     u"Jardins", u"Liaison", u"Mail", u"Montée", u"Môle",
+                     u"Parc", u"Passage", u"Passerelle", u"Passerelles",
+                     u"Place", u"Placette", u"Pont", u"Promenade",
+                     u"Petite Avenue", u"Petite Rue", u"Quai",
+                     u"Rampe", u"Rang", u"Résidence", u"Rond-Point",
+                     u"Route forestière", u"Route", u"Rue", u"Ruelle",
+                     u"Square", u"Sente", u"Sentier", u"Sentiers", 
u"Terre-Plein",
+                     u"Télécabine", u"Traboule", u"Traverse", u"Tunnel",
+                     u"Venelle", u"Villa", u"Virage"
+                   ]
+
+    DETERMINANTS = [ # Catalan
+                     u" de", u" de la", u" del", u" dels", u" d'",
+                     u" de l'", u" de sa", u" de son", u" de s'",
+                     u" de ses", u" d'en", u" de na", u" de n'",
+
+                     # Spanish (being distinct from Catalan)
+                     u" de las",  u" de los",
+
+                     # French (being distinct from Catalan and Spanish)
+                     u" du",
+                     u""]
+
+
+    DETERMINANTS = [ u" de", u" de la", u" del", u" de las",
+                     u" dels", u" de los", u" d'", u" de l'", u"de sa", u"de 
son", u"de s'",
+                     u"de ses", u"d'en", u"de na", u"de n'", u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
+    C_ACCENT = re.compile(ur"[ç]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        s = self.N_ACCENT.sub("n", s)
+        s = self.C_ACCENT.sub("c", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_pt_br_generic(i18n):
+    APPELLATIONS = [ u"Aeroporto", u"Aer.", u"Alameda", u"Al.", 
u"Apartamento", u"Ap.", 
+                     u"Área", u"Avenida", u"Av.", u"Beco", u"Bc.", u"Bloco", 
u"Bl.", 
+                     u"Caminho", u"Cam.", u"Campo", u"Chácara", u"Colônia",
+                     u"Condomínio", u"Conjunto", u"Cj.", u"Distrito", 
u"Esplanada", u"Espl.", 
+                     u"Estação", u"Est.", u"Estrada", u"Estr.", u"Favela", 
u"Fazenda",
+                     u"Feira", u"Jardim", u"Jd.", u"Ladeira", u"Lago",
+                     u"Lagoa", u"Largo", u"Loteamento", u"Morro", u"Núcleo",
+                     u"Parque", u"Pq.", u"Passarela", u"Pátio", u"Praça", 
u"Pç.", u"Quadra",
+                     u"Recanto", u"Residencial", u"Resid.", u"Rua", u"R.", 
+                     u"Setor", u"Sítio", u"Travessa", u"Tv.", u"Trecho", 
u"Trevo",
+                     u"Vale", u"Vereda", u"Via", u"V.", u"Viaduto", u"Viela",
+                     u"Vila", u"Vl." ]
+    DETERMINANTS = [ u" do", u" da", u" dos", u" das", u"" ]
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_ar_generic(i18n):
+    APPELLATIONS = [ u"شارع", u"طريق", u"زقاق", u"نهج", u"جادة",
+                     u"ممر", u"حارة",
+                     u"كوبري", u"كوبرى", u"جسر", u"مطلع", u"منزل",
+                     u"مفرق", u"ملف", u"تقاطع",
+                     u"ساحل",
+                     u"ميدان", u"ساحة", u"دوار" ]
+
+    DETERMINANTS = [ u" ال", u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    A_ACCENT = re.compile(ur"[اإآ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.A_ACCENT.sub("أ", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+    def isrtl(self):
+        return True
+
+class i18n_ru_generic(i18n):
+    APPELLATIONS = [ u"ул", u"бул", u"пер", u"пр", u"улица", u"бульвар", 
u"проезд",
+                     u"проспект", u"площадь", u"сквер", u"парк" ]
+    # only "ул." and "пер." are recommended shortenings, however other words 
can 
+    # occur shortened.
+    #
+    # http://bit.ly/6ASISp (OSM wiki)
+    #
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)\.?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        # usually, there are no accents in russian names, only "ё" sometimes, 
but
+        # not as first letter
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_nl_generic(i18n):
+    #
+    # Dutch streets are often named after people and include a title.
+    # The title will be captured as part of the <prefix>
+    #
+    APPELLATIONS = [ u"St.", u"Sint", u"Ptr.", u"Pater",
+                     u"Prof.", u"Professor", u"Past.", u"Pastoor",
+                     u"Pr.", u"Prins", u"Prinses", u"Gen.", u"Generaal",
+                     u"Mgr.", u"Monseigneur", u"Mr.", u"Meester",
+                     u"Burg.", u"Burgermeester", u"Dr.", u"Dokter",
+                     u"Ir.", u"Ingenieur", u"Ds.", u"Dominee", u"Deken",
+                     u"Drs.", u"Maj.", u"Majoor",
+                     # counting words before street name,
+                     # e.g. "1e Walstraat" => "Walstraat (1e)"
+                     u"\d+e",
+                     u"" ]
+    #
+    # Surnames in Dutch streets named after people tend to have the middle name
+    # listed after the rest of the surname,
+    # e.g. "Prins van Oranjestraat" => "Oranjestraat (Prins van)"
+    # Likewise, articles are captured as part of the prefix,
+    # e.g. "Den Urling" => "Urling (Den)"
+    #
+    DETERMINANTS = [ u"\s?van der", u"\s?van den", u"\s?van de", u"\s?van",
+                     u"\s?Den", u"\s?D'n", u"\s?D'", u"\s?De", u"\s?'T", 
u"\s?Het",
+                     u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)),
+                                      re.IGNORECASE | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        #
+        # Make sure name actually contains something,
+        # the PREFIX_REGEXP.match fails on zero-length strings
+        #
+        if len(name) == 0:
+            return name
+
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        matches = self.PREFIX_REGEXP.match(name)
+        #
+        # If no prefix was captured, that's okay. Don't substitute
+        # the name however, "<name> ()" looks silly
+        #
+        if matches == None:
+            return name
+
+        if matches.group('prefix'):
+            name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_hr_HR(i18n):
+    # for upper_unaccent_string
+    C_ACCENT = re.compile(ur"[ćč]", re.IGNORECASE | re.UNICODE)
+    D_ACCENT = re.compile(ur"đ|dž", re.IGNORECASE | re.UNICODE)
+    N_ACCENT = re.compile(ur"nj", re.IGNORECASE | re.UNICODE)
+    L_ACCENT = re.compile(ur"lj", re.IGNORECASE | re.UNICODE)
+    S_ACCENT = re.compile(ur"š", re.IGNORECASE | re.UNICODE)
+    Z_ACCENT = re.compile(ur"ž", re.IGNORECASE | re.UNICODE)
+
+    def upper_unaccent_string(self, s):
+        s = self.C_ACCENT.sub("c", s)
+        s = self.D_ACCENT.sub("d", s)
+        s = self.N_ACCENT.sub("n", s)
+        s = self.L_ACCENT.sub("l", s)
+        s = self.S_ACCENT.sub("s", s)
+        s = self.Z_ACCENT.sub("z", s)
+        return s.upper()
+
+    def __init__(self, language, locale_path):
+        """Install the _() function for the chosen locale other
+           object initialisation"""
+        self.language = str(language) # FIXME: why do we have unicode here?
+        _install_language(language, locale_path)
+
+    def language_code(self):
+        """returns the language code of the specific language
+           supported, e.g. fr_FR.UTF-8"""
+        return self.language
+
+    def user_readable_street(self, name):
+        """ transforms a street name into a suitable form for
+            the map index, e.g. Paris (Rue de) for French"""
+        return name
+
+    ## FIXME: only first letter does not work for Croatian digraphs (dž, lj, 
nj)
+    def first_letter_equal(self, a, b):
+        """returns True if the letters a and b are equal in the map index,
+           e.g. É and E are equals in French map index"""
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_pl_generic(i18n):
+
+    APPELLATIONS = [ u"Dr.", u"Doktora", u"Ks.", u"Księdza",
+                     u"Generała", u"Gen.",
+                     u"Aleja", u"Plac", u"Pl.",
+                     u"Rondo", u"rondo", u"Profesora",
+                     u"Prof.",
+                     u"" ]
+
+    DETERMINANTS = [ u"\s?im.", u"\s?imienia", u"\s?pw.",
+                     u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)),
+                                      re.IGNORECASE | re.UNICODE)
+
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        #
+        # Make sure name actually contains something,
+        # the PREFIX_REGEXP.match fails on zero-length strings
+        #
+        if len(name) == 0:
+            return name
+
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        matches = self.PREFIX_REGEXP.match(name)
+        #
+        # If no prefix was captured, that's okay. Don't substitute
+        # the name however, "<name> ()" looks silly
+        #
+        if matches == None:
+            return name
+
+        if matches.group('prefix'):
+            name = self.PREFIX_REGEXP.sub(r"\g<name>, \g<prefix>", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return a == b
+
+class i18n_tr_TR_generic(i18n):
+    APPELLATIONS = [ u"Sokak", u"Sokağı" ]
+    DETERMINANTS = []
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        #
+        # Make sure name actually contains something,
+        # the PREFIX_REGEXP.match fails on zero-length strings
+        #
+        if len(name) == 0:
+            return name
+
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_de_generic(i18n):
+    #
+    # German streets are often named after people and include a title.
+    # The title will be captured as part of the <prefix>
+       # Covering airport names and "New"/"Old" as prefixes as well
+    #
+    APPELLATIONS = [ u"Alte", u"Alter", u"Doktor", u"Dr.",
+                     u"Flughafen", u"Flugplatz", u"Gen.,", u"General",
+                     u"Neue", u"Neuer", u"Platz",
+                     u"Prinz", u"Prinzessin", u"Prof.",
+                     u"Professor" ]
+    #
+    # Surnames in german streets named after people tend to have the middle 
name
+    # listed after the rest of the surname,
+    # e.g. "Platz der deutschen Einheit" => "deutschen Einheit (Platz der)"
+    # Likewise, articles are captured as part of the prefix,
+    # e.g. "An der Märchenwiese" => "Märchenwiese (An der)"
+    #
+    DETERMINANTS = [ u"\s?An den", u"\s?An der", u"\s?Am",
+                     u"\s?Auf den" , u"\s?Auf der"
+                     u" an", u" des", u" der", u" von", u" vor"]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        #
+        # Make sure name actually contains something,
+        # the PREFIX_REGEXP.match fails on zero-length strings
+        #
+        if len(name) == 0:
+            return name
+
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_ast_generic(i18n):
+
+    APPELLATIONS = [ # Asturian
+                     u"Accesu", u"Autopista", u"Autovia", u"Avenida",
+                     u"Baxada", u"Barrancu", u"Barriu", u"Barriada",
+                     u"Biblioteca", u"Cai", u"Caleya",
+                     u"Calzada", u"Camín", u"Carretera", u"Cuesta",
+                     u"Estación", u"Hospital", u"Iglesia", u"Monasteriu",
+                     u"Monumentu", u"Muelle", u"Muséu",
+                     u"Palaciu", u"Parque", u"Pasadizu", u"Pasaxe",
+                     u"Paséu", u"Planta", u"Plaza", u"Polígonu",
+                     u"Ronda", u"Travesía", u"Urbanización", u"Via",
+                     u"Xardín", u"Xardinos",
+
+                     # Spanish (different from Asturian)
+                     u"Acceso", u"Acequia", u"Alameda", u"Alquería",
+                     u"Andador", u"Angosta", u"Apartamentos", u"Apeadero",
+                     u"Arboleda", u"Arrabal", u"Arroyo", u"Autovía",
+                     u"Bajada", u"Balneario", u"Banda",
+                     u"Barranco", u"Barranquil", u"Barrio", u"Bloque",
+                     u"Brazal", u"Bulevar", u"Calle", u"Calleja",
+                     u"Callejón", u"Callejuela", u"Callizo",
+                     u"Camino", u"Camping", u"Cantera", u"Cantina",
+                     u"Cantón", u"Carrera", u"Carrero", u"Carreterín",
+                     u"Carretil", u"Carril", u"Caserío", u"Chalet",
+                     u"Cinturón", u"Circunvalación", u"Cobertizo",
+                     u"Colonia", u"Complejo", u"Conjunto", u"Convento",
+                     u"Cooperativa", u"Corral", u"Corralillo", u"Corredor",
+                     u"Cortijo", u"Costanilla", u"Costera", u"Cuadra",
+                     u"Dehesa", u"Demarcación", u"Diagonal",
+                     u"Diseminado", u"Edificio", u"Empresa", u"Entrada",
+                     u"Escalera", u"Escalinata", u"Espalda", u"Estación",
+                     u"Estrada", u"Explanada", u"Extramuros", u"Extrarradio",
+                     u"Fábrica", u"Galería", u"Glorieta", u"Gran Vía",
+                     u"Granja", u"Hipódromo", u"Jardín", u"Ladera",
+                     u"Llanura", u"Malecón", u"Mercado", u"Mirador",
+                     u"Monasterio", u"Núcleo", u"Palacio",
+                     u"Pantano", u"Paraje", u"Particular",
+                     u"Partida", u"Pasadizo", u"Pasaje", u"Paseo",
+                     u"Paseo marítimo", u"Pasillo", u"Plaza", u"Plazoleta",
+                     u"Plazuela", u"Poblado", u"Polígono", u"Polígono 
industrial",
+                     u"Portal", u"Pórtico", u"Portillo", u"Prazuela",
+                     u"Prolongación", u"Pueblo", u"Puente", u"Puerta",
+                     u"Puerto", u"Punto kilométrico", u"Rampla",
+                     u"Residencial", u"Ribera", u"Rincón", u"Rinconada",
+                     u"Sanatorio", u"Santuario", u"Sector", u"Sendera",
+                     u"Sendero", u"Subida", u"Torrente", u"Tránsito",
+                     u"Transversal", u"Trasera", u"Travesía", u"Urbanización",
+                     u"Vecindario", u"Vereda", u"Viaducto", u"Viviendas",
+                   ]
+
+    DETERMINANTS = [ # Asturian
+                     u" de", u" de la", u" del", u" de les", u" d'",
+                     u" de los", u" de l'",
+
+                     # Spanish (different from Asturian)
+                     u" de las",
+                     u""]
+
+
+    DETERMINANTS = [ u" de", u" de la", u" del", u" de les",
+                     u" de los", u" de las", u" d'", u" de l'", u"" ]
+
+    SPACE_REDUCE = re.compile(r"\s+")
+    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
+                                    ("|".join(APPELLATIONS),
+                                     "|".join(DETERMINANTS)), re.IGNORECASE
+                                                                 | re.UNICODE)
+
+    # for IndexPageGenerator.upper_unaccent_string
+    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
+    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
+    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
+    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
+    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
+    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
+    H_ACCENT = re.compile(ur"[ḥ]", re.IGNORECASE | re.UNICODE)
+    L_ACCENT = re.compile(ur"[ḷ]", re.IGNORECASE | re.UNICODE)
+
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def upper_unaccent_string(self, s):
+        s = self.E_ACCENT.sub("e", s)
+        s = self.I_ACCENT.sub("i", s)
+        s = self.A_ACCENT.sub("a", s)
+        s = self.O_ACCENT.sub("o", s)
+        s = self.U_ACCENT.sub("u", s)
+        s = self.N_ACCENT.sub("n", s)
+        s = self.H_ACCENT.sub("h", s)
+        s = self.L_ACCENT.sub("l", s)
+        return s.upper()
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        name = name.strip()
+        name = self.SPACE_REDUCE.sub(" ", name)
+        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
+        return name
+
+    def first_letter_equal(self, a, b):
+        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
+
+class i18n_generic(i18n):
+    def __init__(self, language, locale_path):
+        self.language = str(language)
+        _install_language(language, locale_path)
+
+    def language_code(self):
+        return self.language
+
+    def user_readable_street(self, name):
+        return name
+
+    def first_letter_equal(self, a, b):
+        return a == b
+
+# When not listed in the following map, default language class will be
+# i18n_generic
+language_class_map = {
+    'fr_BE.UTF-8': i18n_fr_generic,
+    'fr_FR.UTF-8': i18n_fr_generic,
+    'fr_CA.UTF-8': i18n_fr_generic,
+    'fr_CH.UTF-8': i18n_fr_generic,
+    'fr_LU.UTF-8': i18n_fr_generic,
+    'en_AG': i18n_generic,
+    'en_AU.UTF-8': i18n_generic,
+    'en_BW.UTF-8': i18n_generic,
+    'en_CA.UTF-8': i18n_generic,
+    'en_DK.UTF-8': i18n_generic,
+    'en_GB.UTF-8': i18n_generic,
+    'en_HK.UTF-8': i18n_generic,
+    'en_IE.UTF-8': i18n_generic,
+    'en_IN': i18n_generic,
+    'en_NG': i18n_generic,
+    'en_NZ.UTF-8': i18n_generic,
+    'en_PH.UTF-8': i18n_generic,
+    'en_SG.UTF-8': i18n_generic,
+    'en_US.UTF-8': i18n_generic,
+    'en_ZA.UTF-8': i18n_generic,
+    'en_ZW.UTF-8': i18n_generic,
+    'nl_BE.UTF-8': i18n_nl_generic,
+    'nl_NL.UTF-8': i18n_nl_generic,
+    'it_IT.UTF-8': i18n_it_generic,
+    'it_CH.UTF-8': i18n_it_generic,
+    'de_AT.UTF-8': i18n_de_generic,
+    'de_BE.UTF-8': i18n_de_generic,
+    'de_DE.UTF-8': i18n_de_generic,
+    'de_LU.UTF-8': i18n_de_generic,
+    'de_CH.UTF-8': i18n_de_generic,
+    'es_ES.UTF-8': i18n_es_generic,
+    'es_AR.UTF-8': i18n_es_generic,
+    'es_BO.UTF-8': i18n_es_generic,
+    'es_CL.UTF-8': i18n_es_generic,
+    'es_CR.UTF-8': i18n_es_generic,
+    'es_DO.UTF-8': i18n_es_generic,
+    'es_EC.UTF-8': i18n_es_generic,
+    'es_SV.UTF-8': i18n_es_generic,
+    'es_GT.UTF-8': i18n_es_generic,
+    'es_HN.UTF-8': i18n_es_generic,
+    'es_MX.UTF-8': i18n_es_generic,
+    'es_NI.UTF-8': i18n_es_generic,
+    'es_PA.UTF-8': i18n_es_generic,
+    'es_PY.UTF-8': i18n_es_generic,
+    'es_PE.UTF-8': i18n_es_generic,
+    'es_PR.UTF-8': i18n_es_generic,
+    'es_US.UTF-8': i18n_es_generic,
+    'es_UY.UTF-8': i18n_es_generic,
+    'es_VE.UTF-8': i18n_es_generic,
+    'ca_ES.UTF-8': i18n_ca_generic,
+    'ca_AD.UTF-8': i18n_ca_generic,
+    'ca_FR.UTF-8': i18n_ca_generic,
+    'pt_BR.UTF-8': i18n_pt_br_generic,
+    'da_DK.UTF-8': i18n_generic,
+    'ar_AE.UTF-8': i18n_ar_generic,
+    'ar_BH.UTF-8': i18n_ar_generic,
+    'ar_DZ.UTF-8': i18n_ar_generic,
+    'ar_EG.UTF-8': i18n_ar_generic,
+    'ar_IN': i18n_ar_generic,
+    'ar_IQ.UTF-8': i18n_ar_generic,
+    'ar_JO.UTF-8': i18n_ar_generic,
+    'ar_KW.UTF-8': i18n_ar_generic,
+    'ar_LB.UTF-8': i18n_ar_generic,
+    'ar_LY.UTF-8': i18n_ar_generic,
+    'ar_MA.UTF-8': i18n_ar_generic,
+    'ar_OM.UTF-8': i18n_ar_generic,
+    'ar_QA.UTF-8': i18n_ar_generic,
+    'ar_SA.UTF-8': i18n_ar_generic,
+    'ar_SD.UTF-8': i18n_ar_generic,
+    'ar_SY.UTF-8': i18n_ar_generic,
+    'ar_TN.UTF-8': i18n_ar_generic,
+    'ar_YE.UTF-8': i18n_ar_generic,
+    'hr_HR.UTF-8': i18n_hr_HR,
+    'ru_RU.UTF-8': i18n_ru_generic,
+    'pl_PL.UTF-8': i18n_pl_generic,
+    'nb_NO.UTF-8': i18n_generic,
+    'nn_NO.UTF-8': i18n_generic,
+    'tr_TR.UTF-8': i18n_tr_TR_generic,
+    'ast_ES.UTF-8': i18n_ast_generic,
+    'sk_SK.UTF-8': i18n_generic,
+}
+
+def install_translation(locale_name, locale_path):
+    """Return a new i18n class instance, depending on the specified
+    locale name (eg. "fr_FR.UTF-8"). See output of "locale -a" for a
+    list of system-supported locale names. When none matching, default
+    class is i18n_generic"""
+    language_class = language_class_map.get(locale_name, i18n_generic)
+    return language_class(locale_name, locale_path)
diff --git a/ocitysmap/indexlib/__init__.py b/ocitysmap/indexlib/__init__.py
new file mode 100644
index 0000000..bfbbd8b
--- /dev/null
+++ b/ocitysmap/indexlib/__init__.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+if __name__ == '__main__':
+    import cairo
+    import logging
+    import os
+    import psycopg2
+    import random
+    import string
+
+    from ocitysmap import i18n, coords
+    from ocitysmap.maplib.grid import Grid
+    from indexer  import StreetIndex
+    from renderer import StreetIndexRenderer
+    from commons  import IndexCategory, IndexItem
+
+
+    logging.basicConfig(level=logging.DEBUG)
+    random.seed(42)
+
+    lang = "fr_FR.UTF-8"
+    #lang = "ar_MA.UTF-8"
+    #lang = "zh_CN.utf8"
+    i18n = i18n.install_translation(lang,
+                os.path.join(os.path.dirname(__file__),
+                             "..", "..", "locale"))
+
+    bbox = coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699) # France
+    #bbox = coords.BoundingBox(34.0322, -6.8648, 34.0073, -6.8133) # Moroco
+    #bbox = bbox = coords.BoundingBox(22.5786, 114.0308, 22.5231, 114.1338) # 
CN
+
+    # Build the list of index items
+    db = psycopg2.connect(user='maposmatic',
+                          password='waeleephoo3Aew3u',
+                          host='localhost',
+                          database='maposmatic')
+
+    street_index = StreetIndex(db, bbox.as_wkt(), i18n)
+    print street_index.categories
+
+    # Render the items
+    class i18nMock:
+        def __init__(self, rtl):
+            self.rtl = rtl
+        def isrtl(self):
+            return self.rtl
+
+    width  = 2.5*(20 / 2.54) * 72
+    height = 2.5*(29 / 2.54) * 72
+
+    surface = cairo.PDFSurface('/tmp/myindex.pdf', width, height)
+
+    # Map index to grid
+    grid = Grid(bbox, rtl = False)
+    street_index.apply_grid(grid)
+
+    index = StreetIndexRenderer(i18nMock(False), street_index.categories)
+
+    def _render(freedom_dimension, alignment):
+        x,y,w,h = 50, 50, width-100, height-100
+
+        # Draw constraining rectangle
+        ctx = cairo.Context(surface)
+
+        ctx.save()
+        ctx.set_source_rgb(.2,0,0)
+        ctx.rectangle(x,y,w,h)
+        ctx.stroke()
+
+        # Precompute index area
+        rendering_area = index.precompute_occupation_area(surface, x,y,w,h,
+                                                          freedom_dimension,
+                                                          alignment)
+
+        # Draw a green background for the precomputed area
+        ctx.set_source_rgba(0,1,0,.5)
+        ctx.rectangle(rendering_area.x, rendering_area.y,
+                      rendering_area.w, rendering_area.h)
+        ctx.fill()
+        ctx.restore()
+
+        # Render the index
+        index.render(ctx, rendering_area)
+
+    _render('height', 'top')
+    surface.show_page()
+    _render('height', 'bottom')
+    surface.show_page()
+    _render('width', 'left')
+    surface.show_page()
+    _render('width', 'right')
+    surface.show_page()
+
+    ##
+    ## Now demo with RTL = True
+    ##
+
+    # Map index to grid
+    grid = Grid(bbox, rtl = True)
+    street_index.apply_grid(grid)
+
+    index = StreetIndexRenderer(i18nMock(True), street_index.categories)
+    _render('height', 'top')
+    surface.show_page()
+    _render('height', 'bottom')
+    surface.show_page()
+    _render('width', 'left')
+    surface.show_page()
+    _render('width', 'right')
+
+    surface.finish()
+    print "Generated /tmp/myindex.pdf."
diff --git a/ocitysmap/indexlib/commons.py b/ocitysmap/indexlib/commons.py
new file mode 100644
index 0000000..e5f4105
--- /dev/null
+++ b/ocitysmap/indexlib/commons.py
@@ -0,0 +1,284 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import pango
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+import draw_utils
+
+
+class IndexEmptyError(Exception):
+    """This exception is raised when no data is to be rendered in the index."""
+    pass
+
+class IndexDoesNotFitError(Exception):
+    """This exception is raised when the index does not fit in the given
+    graphical area, even after trying smaller font sizes."""
+    pass
+
+NUMBER_CATEGORY_NAME = '0-9'
+
+class IndexCategory:
+    """
+    The IndexCategory represents a set of index items that belong to the same
+    category (their first letter is the same or they are of the same amenity
+    type).
+    """
+    name = None
+    items = None
+    is_street = False
+
+    def __init__(self, name, items=None, is_street=True):
+        assert name is not None
+        self.name = name
+        self.items = items or list()
+        self.is_street = is_street
+
+    def __str__(self):
+        return '<%s (%s)>' % (self.name, map(str, self.items))
+
+    def __repr__(self):
+        return 'IndexCategory(%s, %s)' % (repr(self.name),
+                                          repr(self.items))
+
+    def draw(self, rtl, ctx, pc, layout, fascent, fheight,
+             baseline_x, baseline_y):
+        """Draw this category header.
+
+        Args:
+            rtl (boolean): whether to draw right-to-left or not.
+            ctx (cairo.Context): the Cairo context to draw to.
+            pc (pangocairo.CairoContext): the PangoCairo context.
+            layout (pango.layout): the Pango layout to draw text into.
+            fascent (int): font ascent.
+            fheight (int): font height.
+            baseline_x (int): base X axis position.
+            baseline_y (int): base Y axis position.
+        """
+
+        ctx.save()
+        ctx.set_source_rgb(0.9, 0.9, 0.9)
+        ctx.rectangle(baseline_x, baseline_y - fascent,
+                      layout.get_width() / pango.SCALE, fheight)
+        ctx.fill()
+
+        ctx.set_source_rgb(0.0, 0.0, 0.0)
+        draw_utils.draw_text_center(ctx, pc, layout, fascent, fheight,
+                                    baseline_x, baseline_y, self.name)
+        ctx.restore()
+
+    def get_all_item_labels(self):
+        return [x.label for x in self.items]
+
+    def get_all_item_squares(self):
+        return [x.squares for x in self.items]
+
+
+class IndexItem:
+    """
+    An IndexItem represents one item in the index (a street or a POI). It
+    contains the item label (street name, POI name or description) and the
+    humanized squares description.
+    """
+    __slots__    = ['label', 'endpoint1', 'endpoint2', 'location_str']
+    label        = None # str
+    endpoint1    = None # coords.Point
+    endpoint2    = None # coords.Point
+    location_str = None # str or None
+    page_number  = None # integer or None. Only used by multi-page renderer.
+
+    def __init__(self, label, endpoint1, endpoint2, page_number=None):
+        assert label is not None
+        self.label        = label
+        self.endpoint1    = endpoint1
+        self.endpoint2    = endpoint2
+        self.location_str = None
+        self.page_number  = page_number
+
+    def __str__(self):
+        return '%s...%s' % (self.label, self.location_str)
+
+    def __repr__(self):
+        return ('IndexItem(%s, %s, %s, %s, %s)'
+                % (repr(self.label), self.endpoint1, self.endpoint2,
+                   repr(self.location_str), repr(self.page_number)))
+
+    def label_drawing_width(self, layout):
+        layout.set_text(self.label)
+        return float(layout.get_size()[0]) / pango.SCALE
+
+    def label_drawing_height(self, layout):
+        layout.set_text(self.label)
+        return float(layout.get_size()[1]) / pango.SCALE
+
+    def location_drawing_width(self, layout):
+        layout.set_text(self.location_str)
+        return float(layout.get_size()[0]) / pango.SCALE
+
+    def draw(self, rtl, ctx, pc, column_layout, fascent, fheight,
+             baseline_x, baseline_y,
+             label_layout=None, label_height=0, location_width=0):
+        """Draw this index item to the provided Cairo context. It prints the
+        label, the squares definition and the dotted line, with respect to the
+        RTL setting.
+
+        Args:
+            rtl (boolean): right-to-left localization.
+            ctx (cairo.Context): the Cairo context to draw to.
+            pc (pangocairo.PangoCairo): the PangoCairo context for text
+                drawing.
+            column_layout (pango.Layout): the Pango layout to use for text
+                rendering, pre-configured with the appropriate font.
+            fascent (int): font ascent.
+            fheight (int): font height.
+            baseline_x (int): X axis coordinate of the baseline.
+            baseline_y (int): Y axis coordinate of the baseline.
+        Optional args (in case of label wrapping):
+            label_layout (pango.Layout): the Pango layout to use for text
+                rendering, in case the label should be wrapped
+            label_height (int): height of the big label
+            location_width (int): width of the 'location' part
+        """
+
+        # Fallbacks in case we dont't have a wrapping label
+        if label_layout == None:
+            label_layout = column_layout
+        if label_height == 0:
+            label_height = fheight
+
+        if not self.location_str:
+            location_str = '???'
+        else:
+            location_str = self.location_str
+
+        ctx.save()
+        if not rtl:
+            _, _, line_start = draw_utils.draw_text_left(ctx, pc, label_layout,
+                                                         fascent, fheight,
+                                                         baseline_x, 
baseline_y,
+                                                         self.label)
+            line_end, _, _ = draw_utils.draw_text_right(ctx, pc, column_layout,
+                                                        fascent, fheight,
+                                                        baseline_x, baseline_y,
+                                                        location_str)
+        else:
+            _, _, line_start = draw_utils.draw_text_left(ctx, pc, 
column_layout,
+                                                         fascent, fheight,
+                                                         baseline_x, 
baseline_y,
+                                                         location_str)
+            line_end, _, _ = draw_utils.draw_text_right(ctx, pc, label_layout,
+                                                        fascent, fheight,
+                                                        (baseline_x
+                                                         + location_width),
+                                                        baseline_y,
+                                                        self.label)
+
+        # In case of empty label, we don't draw the dots
+        if self.label != '':
+            draw_utils.draw_dotted_line(ctx, max(fheight/12, 1),
+                                        line_start + fheight/4, baseline_y,
+                                        line_end - line_start - fheight/2)
+        ctx.restore()
+
+    def update_location_str(self, grid):
+        """
+        Update the location_str field from the given Grid object.
+
+        Args:
+           grid (ocitysmap.Grid): the Grid object from which we
+           compute the location strings
+
+        Returns:
+           Nothing, but the location_str field will have been altered
+        """
+        if self.endpoint1 is not None:
+            ep1_label = grid.get_location_str( * self.endpoint1.get_latlong())
+        else:
+            ep1_label = None
+        if self.endpoint2 is not None:
+            ep2_label = grid.get_location_str( * self.endpoint2.get_latlong())
+        else:
+            ep2_label = None
+        if ep1_label is None:
+            ep1_label = ep2_label
+        if ep2_label is None:
+            ep2_label = ep1_label
+
+        if ep1_label == ep2_label:
+            if ep1_label is None:
+                self.location_str = "???"
+            self.location_str = ep1_label
+        elif grid.rtl:
+            self.location_str = "%s-%s" % (max(ep1_label, ep2_label),
+                                           min(ep1_label, ep2_label))
+        else:
+            self.location_str = "%s-%s" % (min(ep1_label, ep2_label),
+                                           max(ep1_label, ep2_label))
+
+        if self.page_number is not None:
+            if grid.rtl:
+                self.location_str = "%s, %d" % (self.location_str,
+                                                self.page_number)
+            else:
+                self.location_str = "%d, %s" % (self.page_number,
+                                                self.location_str)
+
+if __name__ == "__main__":
+    import cairo
+    import pangocairo
+
+    surface = cairo.PDFSurface('/tmp/idx_commons.pdf', 1000, 1000)
+
+    ctx = cairo.Context(surface)
+    pc = pangocairo.CairoContext(ctx)
+
+    font_desc = pango.FontDescription('DejaVu')
+    font_desc.set_size(12 * pango.SCALE)
+
+    layout = pc.create_layout()
+    layout.set_font_description(font_desc)
+    layout.set_width(200 * pango.SCALE)
+
+    font = layout.get_context().load_font(font_desc)
+    font_metric = font.get_metrics()
+
+    fascent = font_metric.get_ascent() / pango.SCALE
+    fheight = ((font_metric.get_ascent() + font_metric.get_descent())
+               / pango.SCALE)
+
+    first_item  = IndexItem('First Item', None, None)
+    second_item = IndexItem('Second Item', None, None)
+    category    = IndexCategory('Hello world !', [first_item, second_item])
+
+    category.draw(False, ctx, pc, layout, fascent, fheight,
+                  72, 80)
+    first_item.draw(False, ctx, pc, layout, fascent, fheight,
+                    72, 100)
+    second_item.draw(False, ctx, pc, layout, fascent, fheight,
+                     72, 120)
+
+    surface.finish()
+    print "Generated /tmp/idx_commons.pdf"
diff --git a/ocitysmap/indexlib/indexer.py b/ocitysmap/indexlib/indexer.py
new file mode 100644
index 0000000..98ac775
--- /dev/null
+++ b/ocitysmap/indexlib/indexer.py
@@ -0,0 +1,486 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import csv
+import datetime
+from itertools import groupby
+import locale
+import logging
+import os
+import psycopg2
+
+import psycopg2.extensions
+# compatibility with django: see http://code.djangoproject.com/ticket/5996
+psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
+# SQL string escaping routine
+_sql_escape_unicode = lambda s: psycopg2.extensions.adapt(s.encode('utf-8'))
+
+import commons
+import ocitysmap
+
+l = logging.getLogger('ocitysmap')
+
+
+class StreetIndex:
+
+    def __init__(self, db, polygon_wkt, i18n, page_number=None):
+        """
+        Prepare the index of the streets inside the given WKT. This
+        constructor will perform all the SQL queries.
+
+        Args:
+           db (psycopg2 DB): The GIS database
+           polygon_wkt (str): The WKT of the surrounding polygon of interest
+           i18n (i18n.i18n): Internationalization configuration
+
+        Note: All the arguments have to be provided !
+        """
+        self._i18n = i18n
+        self._page_number = page_number
+
+        # Build the contents of the index
+        self._categories = \
+            (self._list_streets(db, polygon_wkt)
+             + self._list_amenities(db, polygon_wkt)
+             + self._list_villages(db, polygon_wkt))
+
+    @property
+    def categories(self):
+        return self._categories
+
+    def apply_grid(self, grid):
+        """
+        Update the location_str field of the streets and amenities by
+        mapping them onto the given grid.
+
+        Args:
+           grid (ocitysmap.Grid): the Grid object from which we
+           compute the location strings
+
+        Returns:
+           Nothing, but self._categories has been modified!
+        """
+        for category in self._categories:
+            for item in category.items:
+                item.update_location_str(grid)
+        self.group_identical_grid_locations()
+
+    def group_identical_grid_locations(self):
+        """
+        Group locations whith the same name and the same position on the grid.
+
+        Returns:
+           Nothing, but self._categories has been modified!
+        """
+        categories = []
+        for category in self._categories:
+            if category.is_street:
+                categories.append(category)
+                continue
+            grouped_items = []
+            sort_key = lambda item:(item.label, item.location_str)
+            items = sorted(category.items, key=sort_key)
+            for label, same_items in groupby(items, key=sort_key):
+                grouped_items.append(same_items.next())
+            category.items = grouped_items
+
+    def write_to_csv(self, title, output_filename):
+        # TODO: implement writing the index to CSV
+        try:
+            fd = open(output_filename, 'w')
+        except Exception,ex:
+            l.warning('error while opening destination file %s: %s'
+                      % (output_filename, ex))
+            return
+
+        l.debug("Creating CSV file %s..." % output_filename)
+        writer = csv.writer(fd)
+
+        # Try to treat indifferently unicode and str in CSV rows
+        def csv_writerow(row):
+            _r = []
+            for e in row:
+                if type(e) is unicode:
+                    _r.append(e.encode('UTF-8'))
+                else:
+                    _r.append(e)
+            return writer.writerow(_r)
+
+        copyright_notice = (u'© %(year)d MapOSMatic/ocitysmap authors. '
+                            u'Map data © %(year)d OpenStreetMap.org '
+                            u'and contributors (CC-BY-SA)' %
+                            {'year': datetime.date.today().year})
+        if title is not None:
+            csv_writerow(['# (UTF-8)', title, copyright_notice])
+        else:
+            csv_writerow(['# (UTF-8)', '', copyright_notice])
+
+        for category in self._categories:
+            csv_writerow(['%s' % category.name])
+            for item in category.items:
+                csv_writerow(['', item.label, item.location_str or '???'])
+
+        fd.close()
+
+    def _get_selected_amenities(self):
+        """
+        Return the kinds of amenities to retrieve from DB as a list of
+        string tuples:
+          1. Category, displayed headers in the final index
+          2. db_amenity, description string stored in the DB
+          3. Label, text to display in the index for this amenity
+
+        Note: This has to be a function because gettext() has to be
+        called, which takes i18n into account... It cannot be
+        statically defined as a class attribute for example.
+        """
+
+        # Make sure gettext is available...
+        try:
+            selected_amenities = [
+                (_(u"Places of worship"), "place_of_worship",
+                 _(u"Place of worship")),
+                (_(u"Education"), "kindergarten", _(u"Kindergarten")),
+                (_(u"Education"), "school", _(u"School")),
+                (_(u"Education"), "college", _(u"College")),
+                (_(u"Education"), "university", _(u"University")),
+                (_(u"Education"), "library", _(u"Library")),
+                (_(u"Public buildings"), "townhall", _(u"Town hall")),
+                (_(u"Public buildings"), "post_office", _(u"Post office")),
+                (_(u"Public buildings"), "public_building",
+                 _(u"Public building")),
+                (_(u"Public buildings"), "police", _(u"Police"))]
+        except NameError:
+            l.exception("i18n has to be initialized beforehand")
+            return []
+
+        return selected_amenities
+
+    def _convert_street_index(self, sl):
+        """Given a list of street names, do some cleanup and pass it
+        through the internationalization layer to get proper sorting,
+        filtering of common prefixes, etc.
+
+        Args:
+            sl (list of tuple): list tuples of the form (street_name,
+                                linestring_wkt) where linestring_wkt
+                                is a WKT for the linestring between
+                                the 2 most distant point of the
+                                street, in 4326 SRID
+
+        Returns the list of IndexCategory objects. Each IndexItem will
+        have its square location still undefined at that point
+        """
+
+        # Street prefixes are postfixed, a human readable label is
+        # built to represent the list of squares, and the list is
+        # alphabetically-sorted.
+        prev_locale = locale.getlocale(locale.LC_COLLATE)
+        locale.setlocale(locale.LC_COLLATE, self._i18n.language_code())
+        try:
+            sorted_sl = sorted([(self._i18n.user_readable_street(name),
+                                 linestring) for name,linestring in sl],
+                               lambda x,y: locale.strcoll(x[0].lower(),
+                                                          y[0].lower()))
+        finally:
+            locale.setlocale(locale.LC_COLLATE, prev_locale)
+
+        result = []
+        current_category = None
+        NUMBER_LIST = [str(i) for i in xrange(10)]
+        for street_name, linestring in sorted_sl:
+            # Create new category if needed
+            if (not current_category
+               or (not self._i18n.first_letter_equal(street_name[0],
+                                                     current_category.name)
+                   and (current_category.name != commons.NUMBER_CATEGORY_NAME
+                        or street_name[0] not in NUMBER_LIST))):
+                if street_name[0] in NUMBER_LIST:
+                    cat_name = commons.NUMBER_CATEGORY_NAME
+                else:
+                    cat_name = self._i18n.upper_unaccent_string(street_name[0])
+                current_category = commons.IndexCategory(cat_name)
+                result.append(current_category)
+
+            # Parse the WKT from the largest linestring in shape
+            try:
+                s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
+                                               linestring[11:-1].split(','))
+            except (ValueError, TypeError):
+                l.exception("Error parsing %s for %s" % (repr(linestring),
+                                                         repr(street_name)))
+                raise
+            endpoint1 = ocitysmap.coords.Point(s_endpoint1[1], s_endpoint1[0])
+            endpoint2 = ocitysmap.coords.Point(s_endpoint2[1], s_endpoint2[0])
+            current_category.items.append(commons.IndexItem(street_name,
+                                                            endpoint1,
+                                                            endpoint2,
+                                                            self._page_number))
+
+        return result
+
+    def _list_streets(self, db, polygon_wkt):
+        """Get the list of streets inside the given polygon. Don't
+        try to map them onto the grid of squares (there location_str
+        field remains undefined).
+
+        Args:
+           db (psycopg2 DB): The GIS database
+           polygon_wkt (str): The WKT of the surrounding polygon of interest
+
+        Returns a list of commons.IndexCategory objects, with their IndexItems
+        having no specific grid square location
+        """
+
+        cursor = db.cursor()
+        l.info("Getting streets...")
+
+        # PostGIS >= 1.5.0 for this to work:
+        query = """
+select name,
+       --- street_kind, -- only when group by is: group by name, street_kind
+       st_astext(st_transform(ST_LongestLine(street_path, street_path),
+                              4002)) as longest_linestring
+from
+  (select name,
+          -- highway as street_kind, -- only when group by name, street_kind
+          st_intersection(%(wkb_limits)s,
+                          st_linemerge(st_collect(%%(way)s))) as street_path
+   from planet_osm_line
+          where trim(name) != '' and highway is not null
+                and st_intersects(%%(way)s, %(wkb_limits)s)
+   group by name ---, street_kind -- (optional)
+   order by name) as foo;
+""" % dict(wkb_limits = ("st_transform(GeomFromText('%s', 4002), 900913)"
+                         % (polygon_wkt,)))
+
+        # l.debug("Street query (nogrid): %s" % query)
+
+        try:
+            cursor.execute(query % {'way':'way'})
+        except psycopg2.InternalError:
+            # This exception generaly occurs when inappropriate ways have
+            # to be cleaned. Using a buffer of 0 generaly helps to clean
+            # them. This operation is not applied by default for
+            # performance.
+            db.rollback()
+            cursor.execute(query % {'way':'st_buffer(way, 0)'})
+        sl = cursor.fetchall()
+
+        l.debug("Got %d streets." % len(sl))
+
+        return self._convert_street_index(sl)
+
+
+    def _list_amenities(self, db, polygon_wkt):
+        """Get the list of amenities inside the given polygon. Don't
+        try to map them onto the grid of squares (there location_str
+        field remains undefined).
+
+        Args:
+           db (psycopg2 DB): The GIS database
+           polygon_wkt (str): The WKT of the surrounding polygon of interest
+
+        Returns a list of commons.IndexCategory objects, with their IndexItems
+        having no specific grid square location
+        """
+
+        cursor = db.cursor()
+
+        result = []
+        for catname, db_amenity, label in self._get_selected_amenities():
+            l.info("Getting amenities for %s/%s..." % (catname, db_amenity))
+
+            # Get the current IndexCategory object, or create one if
+            # different than previous
+            if (not result or result[-1].name != catname):
+                current_category = commons.IndexCategory(catname,
+                                                         is_street=False)
+                result.append(current_category)
+            else:
+                current_category = result[-1]
+
+            query = """
+select amenity_name,
+       st_astext(st_transform(ST_LongestLine(amenity_contour, amenity_contour),
+                              4002)) as longest_linestring
+from (
+       select name as amenity_name,
+              st_intersection(%(wkb_limits)s, %%(way)s) as amenity_contour
+       from planet_osm_point
+       where trim(name) != ''
+             and amenity = %(amenity)s and ST_intersects(%%(way)s, 
%(wkb_limits)s)
+      union
+       select name as amenity_name,
+              st_intersection(%(wkb_limits)s , %%(way)s) as amenity_contour
+       from planet_osm_polygon
+       where trim(name) != '' and amenity = %(amenity)s
+             and ST_intersects(%%(way)s, %(wkb_limits)s)
+     ) as foo
+order by amenity_name""" \
+                % {'amenity': _sql_escape_unicode(db_amenity),
+                   'wkb_limits': ("st_transform(GeomFromText('%s' , 4002), 
900913)"
+                                  % (polygon_wkt,))}
+
+
+            # l.debug("Amenity query for for %s/%s (nogrid): %s" \
+            #             % (catname, db_amenity, query))
+            try:
+                cursor.execute(query % {'way':'way'})
+            except psycopg2.InternalError:
+                # This exception generaly occurs when inappropriate ways have
+                # to be cleaned. Using a buffer of 0 generaly helps to clean
+                # them. This operation is not applied by default for
+                # performance.
+                db.rollback()
+                cursor.execute(query % {'way':'st_buffer(way, 0)'})
+
+            for amenity_name, linestring in cursor.fetchall():
+                # Parse the WKT from the largest linestring in shape
+                try:
+                    s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
+                                                   
linestring[11:-1].split(','))
+                except (ValueError, TypeError):
+                    l.exception("Error parsing %s for %s/%s/%s"
+                                % (repr(linestring), catname, db_amenity,
+                                   repr(amenity_name)))
+                    continue
+                    ## raise
+                endpoint1 = ocitysmap.coords.Point(s_endpoint1[1], 
s_endpoint1[0])
+                endpoint2 = ocitysmap.coords.Point(s_endpoint2[1], 
s_endpoint2[0])
+                current_category.items.append(commons.IndexItem(amenity_name,
+                                                                endpoint1,
+                                                                endpoint2,
+                                                                
self._page_number))
+
+            l.debug("Got %d amenities for %s/%s."
+                    % (len(current_category.items), catname, db_amenity))
+
+        return [category for category in result if category.items]
+
+    def _list_villages(self, db, polygon_wkt):
+        """Get the list of villages inside the given polygon. Don't
+        try to map them onto the grid of squares (there location_str
+        field remains undefined).
+
+        Args:
+           db (psycopg2 DB): The GIS database
+           polygon_wkt (str): The WKT of the surrounding polygon of interest
+
+        Returns a list of commons.IndexCategory objects, with their IndexItems
+        having no specific grid square location
+        """
+
+        cursor = db.cursor()
+
+        result = []
+        current_category = commons.IndexCategory(_(u"Villages"),
+                                                 is_street=False)
+        result.append(current_category)
+
+        query = """
+select village_name,
+       st_astext(st_transform(ST_LongestLine(village_contour, village_contour),
+                              4002)) as longest_linestring
+from (
+       select name as village_name,
+              st_intersection(%(wkb_limits)s, %%(way)s) as village_contour
+       from planet_osm_point
+       where trim(name) != ''
+             and (place = 'locality'
+                  or place = 'hamlet'
+                  or place = 'isolated_dwelling')
+             and ST_intersects(%%(way)s, %(wkb_limits)s)
+     ) as foo
+order by village_name""" \
+            % {'wkb_limits': ("st_transform(GeomFromText('%s', 4002), 900913)"
+                              % (polygon_wkt,))}
+
+
+        # l.debug("Villages query for %s (nogrid): %s" \
+        #             % ('Villages', query))
+
+        try:
+            cursor.execute(query % {'way':'way'})
+        except psycopg2.InternalError:
+            # This exception generaly occurs when inappropriate ways have
+            # to be cleaned. Using a buffer of 0 generaly helps to clean
+            # them. This operation is not applied by default for
+            # performance.
+            db.rollback()
+            cursor.execute(query % {'way':'st_buffer(way, 0)'})
+
+        for village_name, linestring in cursor.fetchall():
+            # Parse the WKT from the largest linestring in shape
+            try:
+                s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
+                                               linestring[11:-1].split(','))
+            except (ValueError, TypeError):
+                l.exception("Error parsing %s for %s/%s"
+                            % (repr(linestring), 'Villages',
+                               repr(village_name)))
+                continue
+                ## raise
+            endpoint1 = ocitysmap.coords.Point(s_endpoint1[1], s_endpoint1[0])
+            endpoint2 = ocitysmap.coords.Point(s_endpoint2[1], s_endpoint2[0])
+            current_category.items.append(commons.IndexItem(village_name,
+                                                            endpoint1,
+                                                            endpoint2,
+                                                            self._page_number))
+
+        l.debug("Got %d villages for %s."
+                % (len(current_category.items), 'Villages'))
+
+        return [category for category in result if category.items]
+
+if __name__ == "__main__":
+    from ocitysmap import i18n
+
+    logging.basicConfig(level=logging.DEBUG)
+
+    db = psycopg2.connect(user='maposmatic',
+                          password='waeleephoo3Aew3u',
+                          host='localhost',
+                          database='maposmatic')
+
+    i18n = i18n.install_translation("fr_FR.UTF-8",
+                                    os.path.join(os.path.dirname(__file__),
+                                                 "..", "..", "locale"))
+
+    # Chevreuse
+    chevreuse_bbox = ocitysmap.coords.BoundingBox(48.7097, 2.0333, 48.7048, 
2.0462)
+    limits_wkt = chevreuse_bbox.as_wkt()
+
+    # Paris envelope:
+    # limits_wkt = """POLYGON((2.22405967037499 
48.8543531620913,2.22407682819692 48.8550025657752,2.22423996225251 
48.8557367772146,2.22466908746374 48.8572219531993,2.22506398686264 
48.8582666566114,2.22559363355415 48.8594446700813,2.2256475324712 
48.8595315483635,2.22678057753906 48.861620896078,2.22753588102995 
48.8635801454558,2.22787059330481 48.8647094580464,2.22819677158448 
48.8653982612096,2.2290979614775 48.8666404585278,2.22973316021491 
48.8672502886549,2.23105485149243 48.8683285824972,2.23214657405722 
48.8695317674313,2.23752344019032 48.8710122798192,2.23998374609047 
48.8716393690211,2.2406936846595 48.8720829886714,2.24189536101507 
48.872839461003,2.24319136047547 48.8738725534154,2.24437587900911 
48.8749394788509,2.24560396583403 48.876357855343,2.2475739712521 
48.8757695828576,2.25479813293547 48.8740773968287,2.25538769725643 
48.8742418854232,2.25841672656296 48.8800895643967,2.25883381434937 
48.8802801449622,2.27745184777039 48.8779547879509,2.27972135150419 
48.8786284619886,2.2799882409751 48.8789087875894,2.28068147087986 
48.8818378450628,2.2818318534327 48.8837294922328,2.28231793183294 
48.8838550796657,2.28449203448356 48.8855611708289,2.28577384056247 
48.8864085830218,2.28619479110461 48.8867093618694,2.28716685807356 
48.8872081947562,2.28783807925385 48.8875526789321,2.28864323924301 
48.8879659741163,2.28891794405689 48.888090074233,2.28971618701836 
48.8884625499349,2.29060021908946 48.8888914901301,2.2910407529048 
48.889100761049,2.29231914538563 48.8897813727734,2.29280746957407 
48.889718645603,2.29453124677277 48.8896871638578,2.29575008095026 
48.8900483462911,2.29616537210611 48.8902642866599,2.29733794304647 
48.8910359587209,2.29849335616491 48.8916923874085,2.30047153625207 
48.8926164760274,2.30323852699021 48.8938978632484,2.30376898216549 
48.8941694805188,2.30599186333604 48.8952257558751,2.30716075118374 
48.895782794086,2.30753112657538 48.8959580779389,2.3095294289249 
48.8967071614408,2.31232139282795 48.8977543476603,2.31338886088006 
48.8980659834922,2.31527002291654 48.8986490922026,2.3161877418108 
48.8989256442189,2.31850782069509 48.8996320466728,2.3186937719589 
48.8997429489435,2.31986508525787 48.9004574890899,2.32026717117904 
48.9006974778028,2.32035754169662 48.9007570614361,2.32190785421396 
48.9008073738244,2.32408474164196 48.9008856768068,2.32755547257369 
48.9008753427098,2.3277815785307 48.9009897853332,2.33018241595904 
48.9010280509197,2.33437611103142 48.9011239509646,2.34387471718264 
48.9013669480994,2.34414043884369 48.9013294504412,2.34815474035383 
48.9014532811833,2.35198506689379 48.9014935541578,2.35509512423894 
48.9015322917683,2.35784441816599 48.901608644928,2.36560774868288 
48.9017625909672,2.37010498448976 48.9018541788921,2.37028114411698 
48.9018568952303,2.3715506432765 48.9018857710774,2.37597825964886 
48.9019807837163,2.37900028209617 48.9020473928421,2.38442916068422 
48.9021559867851,2.38618599588537 48.901825184925,2.38870406345829 
48.9013527757594,2.38942343433781 48.9012220947855,2.38942612928366 
48.901219496516,2.39066068397863 48.9009871870515,2.39373992910953 
48.8992668587557,2.39552739686187 48.8982616923999,2.39644098350582 
48.8967792698801,2.3969293975258 48.8959819963415,2.39776222562571 
48.8945922886365,2.39802974391732 48.893447763192,2.39801492171513 
48.8935286763732,2.39826231774438 48.8926270480788,2.39881388332883 
48.890253950367,2.39895932057332 48.8895456434227,2.39910251202961 
48.8886453608921,2.3991283835098 48.8875843982919,2.39955014253569 
48.8875104454888,2.39916467544727 48.8865343409336,2.39919692496597 
48.8858403943193,2.3992251320659 48.8853650577775,2.39924848826328 
48.8848527974081,2.40007305186258 48.8838178642316,2.4014665185313 
48.8826086429161,2.40379216697036 48.8814466523376,2.40460945421585 
48.8812171456678,2.40718249868415 48.8805096559317,2.40837555121299 
48.8803798656879,2.40929021583528 48.8802754779329,2.41081034495907 
48.8784304903174,2.41084259447777 48.8784022507851,2.41185625344437 
48.8771519560988,2.4124848944802 48.8763725664976,2.41283757306074 
48.8751571978536,2.41313805952328 48.8741215913749,2.41342911367534 
48.873149893187,2.41356727456603 48.8726744951149,2.41362404809199 
48.8724535746873,2.41370256084782 48.8716509499783,2.41378511602243 
48.8708747835672,2.41382086897074 48.8705644554421,2.41395444845349 
48.8692854838219,2.41400888635971 48.8676265832271,2.41392121078798 
48.865438090478,2.41427317071629 48.8635013703719,2.41435258178741 
48.8633375556219,2.41429544893534 48.86108297997,2.41449361728702 
48.8598641666786,2.4146241424978 48.8590365765406,2.41474819983854 
48.8587550757552,2.41485078744398 48.8577987414008,2.41516681476094 
48.8558674588656,2.41526688708359 48.8552628686639,2.41528063130744 
48.8551795884365,2.41534045910536 48.8548472936301,2.41542642787805 
48.8543619690239,2.41549595748104 48.8539192562004,2.41559863491801 
48.8534670821859,2.41571029550783 48.8527875722221,2.41588232288474 
48.8518013915656,2.41603791109195 48.8508464612523,2.41619754171794 
48.8498895620232,2.41633264833667 48.8492345945092,2.41641933576159 
48.8487604470666,2.41590693672352 48.8466070719205,2.4158173746897 
48.8459314888008,2.41570274965944 48.8452938500999,2.41508273245034 
48.8438678534372,2.41483057535009 48.842407466859,2.41397762498782 
48.8397444662905,2.41343683918678 48.8382643887995,2.41308496908999 
48.8372079747027,2.41286182757341 48.8365921613894,2.41272447516647 
48.8361966532922,2.41248094189295 48.8351168985142,2.41227567685053 
48.8345595985272,2.41120937660828 48.8338612648054,2.41185337883546 
48.8337666549157,2.41276912143609 48.8336280511047,2.41377514472278 
48.8335691561952,2.41456503335211 48.8335829929573,2.4152836855794 
48.8336421835075,2.41609872703668 48.8337632844351,2.41802022342941 
48.8342701548677,2.41971058329954 48.8348930923147,2.42037623492507 
48.8350979770248,2.42216028907934 48.835786478119,2.42174365045056 
48.836467342233,2.42157791128064 48.8368203945709,2.42146292692427 
48.8373379951919,2.42136842415638 48.8378079936814,2.42127904178561 
48.8382237692705,2.42101987782615 48.8390223774626,2.42081524160442 
48.8396772420515,2.42053451807814 48.8402191720905,2.42030571717527 
48.8406060755701,2.4200338869703 48.8408907527957,2.41983958137434 
48.8410638641368,2.41957143426203 48.841498295291,2.4194850163317 
48.841891574028,2.41949453847371 48.8421841662022,2.41943623781177 
48.8425948801975,2.41960179731864 48.8427473525507,2.4198688664526 
48.8430295936735,2.41994342662119 48.8433190458415,2.41981406922027 
48.8434354529104,2.42209974262919 48.8444911444825,2.42232683673301 
48.8443736757305,2.4224450550244 48.8442929786174,2.42254979858653 
48.8442016990887,2.42274715845445 48.8439993937374,2.42290418396612 
48.8437964963748,2.42304917205297 48.8435854987992,2.4236784419095 
48.8426801914628,2.42453121260871 48.8419447834817,2.42477411706154 
48.841896067273,2.42472228426964 48.8417684826086,2.4250133384217 
48.8417235500266,2.42756428433401 48.8415158545549,2.42824206321588 
48.8414465634173,2.43078968536165 48.8413390791936,2.43159969625334 
48.8412721527067,2.43229364481032 48.8412155725592,2.43289785167042 
48.8411423789194,2.43343522387338 48.8410234833686,2.43353817080494 
48.8411703438687,2.43713304890893 48.840876563315,2.43794862935538 
48.8445595446304,2.43937416587975 48.844428596988,2.44051242117626 
48.8443530432941,2.4406181528852 48.8448934447891,2.44079565998534 
48.8448677875258,2.44085872171828 48.8452092526994,2.44068292141718 
48.8452065924015,2.440762961309 48.8459171825144,2.44667486402532 
48.8457350430313,2.4465157723885 48.8449324627219,2.44773784050101 
48.8448136944047,2.4508502334659 48.8444805622559,2.45368262155673 
48.8440907918728,2.45684127775875 48.8436784939176,2.45827462962609 
48.8434694467977,2.4598624917223 48.8431756797296,2.46110701771692 
48.842915846081,2.46205977090726 48.8426616866754,2.46269487981313 
48.8424582518045,2.4632613574313 48.8421589805216,2.46424842626549 
48.8417239638795,2.46530412638739 48.8410234833686,2.46573325159861 
48.8406450968431,2.46645423944564 48.8400349425497,2.46722634143235 
48.8390912580791,2.46853994787231 48.8378656419534,2.46941544594822 
48.8370634676784,2.4696986847573 48.8366792567132,2.46979758927008 
48.8364964923528,2.46975833289216 48.8357040524303,2.46962951448042 
48.8352841759955,2.469376818391 48.8346450417393,2.46897805623638 
48.8341124531957,2.46894293210877 48.8340655624265,2.46873425346827 
48.8338930773407,2.4684829946833 48.833685349399,2.4681630147791 
48.83350044538,2.46768493138489 48.833309154447,2.46675804967473 
48.8328283516622,2.46638156573916 48.8325708887733,2.46606769437889 
48.8323229449658,2.46572795153843 48.8319670816639,2.46544597037075 
48.8315933574293,2.46527798541262 48.8312954995835,2.46511368354715 
48.8309088789962,2.46499097367934 48.8304478634571,2.46468033625409 
48.829476440867,2.46453741429239 48.8286916960156,2.46455591958724 
48.8276476765417,2.46476693384748 48.827591554113,2.46508278150138 
48.8275423508361,2.46522525430544 48.8276692620745,2.46540869028646 
48.8275586730823,2.46616695821778 48.827329451485,2.46581778306685 
48.8265121461304,2.46574735514857 48.8262822692052,2.46538704088811 
48.8251054862733,2.46518545893835 48.8250846684754,2.46527987187471 
48.8246208788162,2.46514233980472 48.8243022796837,2.46474366748162 
48.8233780527712,2.46468662446108 48.8233752730267,2.46292781296631 
48.8203007812736,2.46264268769513 48.8194471626414,2.4625531256613 
48.8193715712664,2.46253803396453 48.8192435743433,2.46101026916082 
48.8184906690057,2.45957682746195 48.8177797489504,2.45883877162452 
48.8174406994093,2.45808337830211 48.8173220433582,2.45637532162088 
48.8174689733421,2.45465019694926 48.8172375761677,2.45322654688698 
48.8173483653878,2.45083927401944 48.817916148904,2.44960858208019 
48.8180567481208,2.44732452563879 48.818110751838,2.44506122028045 
48.8180814727215,2.44269299169693 48.8180604745566,2.44198386161164 
48.8180874468456,2.43986159175291 48.8184555934794,2.43736292779013 
48.818326647826,2.43750971250756 48.8192294970277,2.43688735967872 
48.8195204471285,2.43617912790872 48.8196933957274,2.43516259433321 
48.8197602918961,2.43490801178169 48.81967677518,2.43452964138402 
48.8194661491895,2.43436533951855 48.8194927658336,2.43419852237029 
48.8193901438057,2.4343578835017 48.8202087482566,2.43249298097186 
48.8215368818771,2.43293315546108 48.8217982459121,2.43277855540069 
48.821881463367,2.43264317928737 48.8219974470946,2.43221531171754 
48.8217339548627,2.43039254017454 48.8229863438568,2.4305666336766 
48.8231762547516,2.43030064252097 48.8232780410172,2.42970838325415 
48.8234758760205,2.42917622127984 48.8236283474274,2.42852395455204 
48.8237809957994,2.427802966705 48.8239278477077,2.42714207615048 
48.8240239547446,2.42645531411577 48.8241030876619,2.42554235629252 
48.8241716930794,2.42465293432971 48.8241878981385,2.4234599716324 
48.8241482726199,2.42285055454365 48.8241143839052,2.42180338841696 
48.8240634620762,2.42091073251913 48.8240352510058,2.41995806916032 
48.8240860545781,2.41934865207157 48.824153950308,2.41861904039781 
48.8242838863918,2.41781226344114 48.8244929544404,2.4172629436449 
48.8246059750222,2.41659342926365 48.8247020807587,2.41596685435297 
48.8247641797519,2.41512576175245 48.8247641797519,2.41461938142679 
48.8247415875556,2.41388114592631 48.8247076992422,2.41260239411936 
48.8247020807587,2.41152082251728 48.8248094823974,2.41040502510288 
48.8250241667014,2.40984717131144 48.8252106988182,2.4095934870752 
48.8253025452268,2.40872427720629 48.8256969565131,2.40802907100791 
48.826103843296,2.40753122467745 48.8264315976597,2.40699043887641 
48.8268045915822,2.40610649663684 48.827482561693,2.40505924067861 
48.8283357477846,2.40415679314418 48.8288372916559,2.40312867130151 
48.8292981446041,2.40238181197429 48.8295467538789,2.40146121847113 
48.82936159804,2.40029439674858 48.828848586832,2.39983086606198 
48.8286903949966,2.39569089024358 48.8277253844162,2.39434153085531 
48.8274719758729,2.39341455931363 48.8270650410594,2.39270219529332 
48.8267656778066,2.39101857278782 48.8260457675067,2.39024997423073 
48.8257170643678,2.38881231045002 48.8250059510992,2.38717072909982 
48.8244232849018,2.38514421965038 48.8237067712519,2.38151349876655 
48.8224128807427,2.38074516970405 48.8216876439636,2.37905867258964 
48.8211390665207,2.37788565249164 48.8208040616663,2.37602497204364 
48.8201182529016,2.37415387113835 48.8195135859543,2.37390485814159 
48.8195361805072,2.37086046764371 48.8185493450421,2.36695261649473 
48.8172924680276,2.36696115048993 48.8171455966772,2.36538191222045 
48.8164108785601,2.36485657744229 48.8163676977867,2.36431408484222 
48.8162124833021,2.36334076023187 48.8160458519734,2.36025819133442 
48.8157374908686,2.35668163886222 48.8160636567673,2.35617166527543 
48.8159785959341,2.35590899788635 48.8159722666451,2.35584261238685 
48.8159954543165,2.35561435047316 48.8161225722528,2.35535788145954 
48.8162946452976,2.35511291088156 48.8164794945072,2.35462368837783 
48.8169598626578,2.35419456316661 48.8172876768224,2.3538119706871 
48.8174337787999,2.35343063584899 48.8175532626916,2.35333622291263 
48.8175815957113,2.35282121876025 48.817688953459,2.35222904932496 
48.8177341441997,2.35174836081642 48.8177284657843,2.35119904102018 
48.8176380842518,2.35048667699988 48.8174571432466,2.34984294426728 
48.8172593436366,2.34927646664911 48.8170559830563,2.34859832844113 
48.8167620610855,2.34742243373422 48.8161799497174,2.34697615070107 
48.816010360671,2.34665868607966 48.8159651683763,2.34602348734226 
48.8160273964993,2.34526818385137 48.8161007451382,2.34459372873605 
48.8160886781114,2.34447047987907 48.8155624582755,2.34444541688264 
48.8155255469646,2.34366963180328 48.815818293137,2.34304512301776 
48.8160522995573,2.34162892897234 48.8163212636258,2.34029852403656 
48.8164059689672,2.33958903462517 48.8164496820727,2.33873931819792 
48.8164892545265,2.33811445008628 48.8165226751843,2.33675377192543 
48.8165755566752,2.33618729430726 48.8165981525526,2.33513671458248 
48.8166802547645,2.33494276831264 48.8166885951114,2.33370246439986 
48.8167583937045,2.33211541078741 48.8169823992109,2.33203123864528 
48.8169932830019,2.33219437270088 48.8176466610404,2.33234672697307 
48.8182133764852,2.3292783514571 48.8188955440884,2.32736916198376 
48.8193461966713,2.32566811216175 48.8197292984568,2.32132996799168 
48.8206846446699,2.31988844145525 48.8210021430204,2.31418755299918 
48.8222493455562,2.31400735095318 48.8223115658781,2.30921708470061 
48.8234334701976,2.30687382928199 48.8239227614225,2.30459606104757 
48.8243880952424,2.30415274245486 48.8244955566963,2.30400101700337 
48.8245166704496,2.30126582662629 48.8251162500177,2.29764687367268 
48.8259108681886,2.29422959250036 48.8266900974691,2.29222069003049 
48.8271378412956,2.29090331066633 48.8276893691378,2.28938767311896 
48.8283238611162,2.28546904218657 48.829978742085,2.28336285217143 
48.8308616895553,2.27902309103384 48.8324594827402,2.27632769602384 
48.830228648962,2.27575996076428 48.8297178937999,2.27339568476801 
48.8283193666538,2.27272688903898 48.8279749477035,2.27230836394811 
48.8279610502545,2.26783708945293 48.8278866544835,2.26760478512046 
48.8279671414773,2.26729971724997 48.8315588824109,2.26967162892616 
48.8328133911768,2.27002951773535 48.8330080545344,2.26997543915525 
48.8330472591227,2.26779953987406 48.8345744993608,2.26692476045038 
48.8345170838228,2.26617763162858 48.8344520995247,2.26489681369648 
48.8342849966568,2.26296911892829 48.8339027748382,2.26278244901225 
48.8339252446421,2.25745831398633 48.8345391985692,2.25702927860663 
48.8345165516496,2.25675457379275 48.8344826699466,2.25520965116712 
48.8347538411894,2.25511523823076 48.8348385745105,2.25205108479663 
48.8384259797004,2.25170774869504 48.8385728478167,2.25113264725014 
48.8425497709943,2.25252988684306 48.8455598787296,2.25068663371158 
48.8456377952918,2.24249372882582 48.8477268953035,2.24156136739244 
48.8484739820425,2.24035744524866 48.8495405623492,2.23967867821998 
48.8500017568624,2.2380956670263 48.8503630485457,2.2312561639476 
48.8518493888202,2.22409811826915 48.853457152044,2.22405967037499 
48.8543531620913))"""
+
+    # Paris bbox
+    # limits_wkt = """POLYGON((2.22405964791711 
48.8155243047565,2.22405964791711 48.9021584078545,2.46979772401737 
48.9021584078545,2.46979772401737 48.8155243047565,2.22405964791711 
48.8155243047565))"""
+
+    street_index = StreetIndex(db, limits_wkt, i18n)
+
+    print "=> Got %d categories, total %d items" \
+        % (len(street_index.categories),
+           reduce(lambda r,cat: r+len(cat.items), street_index.categories, 0))
diff --git a/ocitysmap/indexlib/multi_page_renderer.py 
b/ocitysmap/indexlib/multi_page_renderer.py
new file mode 100644
index 0000000..022980b
--- /dev/null
+++ b/ocitysmap/indexlib/multi_page_renderer.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2012  David Mentré
+# Copyright (C) 2012  Thomas Petazzoni
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import math
+import pango
+import pangocairo
+
+import draw_utils
+import ocitysmap.layoutlib.commons as UTILS
+from ocitysmap.layoutlib.abstract_renderer import Renderer
+
+# FIXME: refactoring
+# We use the same 10mm as GRAYED_MARGIN_MM in the map multi-page renderer
+PAGE_NUMBER_MARGIN_PT  = UTILS.convert_mm_to_pt(10)
+
+class MultiPageStreetIndexRenderer:
+    """
+    The MultiPageStreetIndexRenderer class encapsulates all the logic
+    related to the rendering of the street index on multiple pages
+    """
+
+    # ctx: Cairo context
+    # surface: Cairo surface
+    def __init__(self, i18n, ctx, surface, index_categories, rendering_area,
+                 page_number):
+        self._i18n           = i18n
+        self.ctx            = ctx
+        self.surface        = surface
+        self.index_categories = index_categories
+        self.rendering_area_x = rendering_area[0]
+        self.rendering_area_y = rendering_area[1]
+        self.rendering_area_w = rendering_area[2]
+        self.rendering_area_h = rendering_area[3]
+        self.page_number      = page_number
+
+    def _create_layout_with_font(self, pc, font_desc):
+        layout = pc.create_layout()
+        layout.set_font_description(font_desc)
+        font = layout.get_context().load_font(font_desc)
+        font_metric = font.get_metrics()
+
+        fascent = float(font_metric.get_ascent()) / pango.SCALE
+        fheight = float((font_metric.get_ascent() + font_metric.get_descent())
+                        / pango.SCALE)
+        em = float(font_metric.get_approximate_char_width()) / pango.SCALE
+
+        return layout, fascent, fheight, em
+
+    def _draw_page_number(self):
+        self.ctx.save()
+        self.ctx.translate(Renderer.PRINT_SAFE_MARGIN_PT,
+                           Renderer.PRINT_SAFE_MARGIN_PT)
+        draw_utils.render_page_number(self.ctx, self.page_number,
+                                      self.rendering_area_w,
+                                      self.rendering_area_h,
+                                      PAGE_NUMBER_MARGIN_PT,
+                                      transparent_background = False)
+        self.ctx.restore()
+
+    def _new_page(self):
+        self.surface.show_page()
+        self.page_number = self.page_number + 1
+        self._draw_page_number()
+
+    def render(self, dpi = UTILS.PT_PER_INCH):
+        self.ctx.save()
+
+        # Create a PangoCairo context for drawing to Cairo
+        pc = pangocairo.CairoContext(self.ctx)
+
+        header_fd = pango.FontDescription("Georgia Bold 12")
+        label_column_fd  = pango.FontDescription("DejaVu 8")
+
+        header_layout, header_fascent, header_fheight, header_em = \
+            self._create_layout_with_font(pc, header_fd)
+        label_layout, label_fascent, label_fheight, label_em = \
+            self._create_layout_with_font(pc, label_column_fd)
+        column_layout, _, _, _ = \
+            self._create_layout_with_font(pc, label_column_fd)
+
+        # By OCitysmap's convention, the default resolution is 72 dpi,
+        # which maps to the default pangocairo resolution (96 dpi
+        # according to pangocairo docs). If we want to render with
+        # another resolution (different from 72), we have to scale the
+        # pangocairo resolution accordingly:
+        pangocairo.context_set_resolution(column_layout.get_context(),
+                                          96.*dpi/UTILS.PT_PER_INCH)
+        pangocairo.context_set_resolution(label_layout.get_context(),
+                                          96.*dpi/UTILS.PT_PER_INCH)
+        pangocairo.context_set_resolution(header_layout.get_context(),
+                                          96.*dpi/UTILS.PT_PER_INCH)
+
+        margin = label_em
+
+        # find largest label and location
+        max_label_drawing_width = 0.0
+        max_location_drawing_width = 0.0
+        for category in self.index_categories:
+            for street in category.items:
+                w = street.label_drawing_width(label_layout)
+                if w > max_label_drawing_width:
+                    max_label_drawing_width = w
+
+                w = street.location_drawing_width(label_layout)
+                if w > max_location_drawing_width:
+                    max_location_drawing_width = w
+
+        # No street to render, bail out
+        if max_label_drawing_width == 0.0:
+            return
+
+        # Find best number of columns
+        max_drawing_width = \
+            max_label_drawing_width + max_location_drawing_width + 2 * margin
+        max_drawing_height = self.rendering_area_h - PAGE_NUMBER_MARGIN_PT
+
+        columns_count = int(math.ceil(self.rendering_area_w / 
max_drawing_width))
+        # following test should not be needed. No time to prove it. ;-)
+        if columns_count == 0:
+            columns_count = 1
+
+        # We have now have several columns
+        column_width = self.rendering_area_w / columns_count
+
+        column_layout.set_width(int(UTILS.convert_pt_to_dots(
+                    (column_width - margin) * pango.SCALE, dpi)))
+        label_layout.set_width(int(UTILS.convert_pt_to_dots(
+                    (column_width - margin - max_location_drawing_width
+                     - 2 * label_em)
+                    * pango.SCALE, dpi)))
+        header_layout.set_width(int(UTILS.convert_pt_to_dots(
+                    (column_width - margin) * pango.SCALE, dpi)))
+
+        if not self._i18n.isrtl():
+            orig_offset_x = offset_x = margin/2.
+            orig_delta_x  = delta_x  = column_width
+        else:
+            orig_offset_x = offset_x = \
+                self.rendering_area_w - column_width + margin/2.
+            orig_delta_x  = delta_x  = - column_width
+
+        actual_n_cols = 0
+        offset_y = margin/2.
+
+        # page number of first page
+        self._draw_page_number()
+
+        for category in self.index_categories:
+            if ( offset_y + header_fheight + label_fheight
+                 + margin/2. > max_drawing_height ):
+                offset_y       = margin/2.
+                offset_x      += delta_x
+                actual_n_cols += 1
+
+                if actual_n_cols == columns_count:
+                    self._new_page()
+                    actual_n_cols = 0
+                    offset_y = margin / 2.
+                    offset_x = orig_offset_x
+                    delta_x  = orig_delta_x
+
+            category.draw(self._i18n.isrtl(), self.ctx, pc, header_layout,
+                          UTILS.convert_pt_to_dots(header_fascent, dpi),
+                          UTILS.convert_pt_to_dots(header_fheight, dpi),
+                          UTILS.convert_pt_to_dots(self.rendering_area_x
+                                                   + offset_x, dpi),
+                          UTILS.convert_pt_to_dots(self.rendering_area_y
+                                                   + offset_y
+                                                   + header_fascent, dpi))
+
+            offset_y += header_fheight
+
+            for street in category.items:
+                label_height = street.label_drawing_height(label_layout)
+                if ( offset_y + label_height + margin/2.
+                     > max_drawing_height ):
+                    offset_y       = margin/2.
+                    offset_x      += delta_x
+                    actual_n_cols += 1
+
+                    if actual_n_cols == columns_count:
+                        self._new_page()
+                        actual_n_cols = 0
+                        offset_y = margin / 2.
+                        offset_x = orig_offset_x
+                        delta_x  = orig_delta_x
+
+                street.draw(self._i18n.isrtl(), self.ctx, pc, column_layout,
+                            UTILS.convert_pt_to_dots(label_fascent, dpi),
+                            UTILS.convert_pt_to_dots(label_fheight, dpi),
+                            UTILS.convert_pt_to_dots(self.rendering_area_x
+                                                     + offset_x, dpi),
+                            UTILS.convert_pt_to_dots(self.rendering_area_y
+                                                     + offset_y
+                                                     + label_fascent, dpi),
+                            label_layout,
+                            UTILS.convert_pt_to_dots(label_height, dpi),
+                            
UTILS.convert_pt_to_dots(max_location_drawing_width,
+                                                     dpi))
+
+                offset_y += label_height
+
+
+        self.ctx.restore()
+
+
+if __name__ == '__main__':
+    import random
+    import string
+
+    import commons
+    import coords
+
+    width = 72*21./2.54
+    height = 72*29.7/2.54
+
+    surface = cairo.PDFSurface('/tmp/myindex_render.pdf', width, height)
+
+    random.seed(42)
+
+    def rnd_str(max_len, letters = string.letters + ' ' * 4):
+        return ''.join(random.choice(letters)
+                       for i in xrange(random.randint(1, max_len)))
+
+    class i18nMock:
+        def __init__(self, rtl):
+            self.rtl = rtl
+        def isrtl(self):
+            return self.rtl
+
+    streets = []
+    for i in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+              'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
+              'Schools', 'Public buildings']:
+        items = []
+        for label, location_str in [(rnd_str(40).capitalize(),
+                                     '%s%d-%s%d' \
+                                         % (rnd_str(2,
+                                                    string.ascii_uppercase),
+                                            random.randint(1,19),
+                                            rnd_str(2,
+                                                    string.ascii_uppercase),
+                                            random.randint(1,19),
+                                            ))]*random.randint(1, 20):
+            item              = commons.IndexItem(label, None, None)
+            item.location_str = location_str
+            item.page_number  = random.randint(1, 100)
+            items.append(item)
+        streets.append(commons.IndexCategory(i, items))
+
+    ctxtmp = cairo.Context(surface)
+
+    rendering_area = \
+        (15, 15, width - 2 * 15, height - 2 * 15)
+
+    mpsir = MultiPageStreetIndexRenderer(i18nMock(False), ctxtmp, surface,
+                                         streets, rendering_area, 1)
+    mpsir.render()
+    surface.show_page()
+
+    mpsir2 = MultiPageStreetIndexRenderer(i18nMock(True), ctxtmp, surface,
+                                          streets, rendering_area,
+                                          mpsir.page_number + 1)
+    mpsir2.render()
+
+    surface.finish()
diff --git a/ocitysmap/indexlib/renderer.py b/ocitysmap/indexlib/renderer.py
new file mode 100644
index 0000000..0f739e1
--- /dev/null
+++ b/ocitysmap/indexlib/renderer.py
@@ -0,0 +1,588 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import logging
+import math
+import pango
+import pangocairo
+
+import commons
+import ocitysmap.layoutlib.commons as UTILS
+
+LOG = logging.getLogger('ocitysmap')
+
+
+class StreetIndexRenderingStyle:
+    """
+    The StreetIndexRenderingStyle class defines how the header and
+    label items should be drawn (font family, size, etc.).
+    """
+    __slots__ = ["header_font_spec", "label_font_spec"]
+    header_font_spec = None
+    label_font_spec  = None
+
+    def __init__(self, header_font_spec, label_font_spec):
+        """
+        Specify how the headers and label should be rendered. The
+        Pango Font Speficication strings below are of the form
+        "serif,monospace bold italic condensed 16". See
+        http://www.pygtk.org/docs/pygtk/class-pangofontdescription.html
+        for more details.
+
+        Args:
+           header_font_spec (str): Pango Font Specification for the headers.
+           label_font_spec (str): Pango Font Specification for the labels.
+        """
+        self.header_font_spec = header_font_spec
+        self.label_font_spec  = label_font_spec
+
+    def __str__(self):
+        return "Style(headers=%s, labels=%s)" % (repr(self.header_font_spec),
+                                                 repr(self.label_font_spec))
+
+
+class StreetIndexRenderingArea:
+    """
+    The StreetIndexRenderingArea class describes the parameters of the
+    Cairo area and its parameters (fonts) where the index should be
+    renedered. It is basically returned by
+    StreetIndexRenderer::precompute_occupation_area() and used by
+    StreetIndexRenderer::render(). All its attributes x,y,w,h may be
+    used by the global map rendering engines.
+    """
+
+    def __init__(self, street_index_rendering_style, x, y, w, h, n_cols):
+        """
+        Describes the Cairo area to use when rendering the index.
+
+        Args:
+             street_index_rendering_style (StreetIndexRenderingStyle):
+                   how to render the text inside the index
+             x (int): horizontal origin position (cairo units).
+             y (int): vertical origin position (cairo units).
+             w (int): width of area to use (cairo units).
+             h (int): height of area to use (cairo units).
+             n_cols (int): number of columns in the index.
+        """
+        self.rendering_style = street_index_rendering_style
+        self.x, self.y, self.w, self.h, self.n_cols = x, y, w, h, n_cols
+
+    def __str__(self):
+        return "Area(%s, %dx%d+%d+%d, n_cols=%d)" \
+            % (self.rendering_style,
+               self.w, self.h, self.x, self.y, self.n_cols)
+
+
+class StreetIndexRenderer:
+    """
+    The StreetIndex class encapsulates all the logic related to the querying 
and
+    rendering of the street index.
+    """
+
+    def __init__(self, i18n, index_categories,
+                 street_index_rendering_styles \
+                     = [ StreetIndexRenderingStyle('Georgia Bold 16',
+                                                   'DejaVu 12'),
+                         StreetIndexRenderingStyle('Georgia Bold 14',
+                                                   'DejaVu 10'),
+                         StreetIndexRenderingStyle('Georgia Bold 12',
+                                                   'DejaVu 8'),
+                         StreetIndexRenderingStyle('Georgia Bold 10',
+                                                   'DejaVu 7'),
+                         StreetIndexRenderingStyle('Georgia Bold 8',
+                                                   'DejaVu 6'),
+                         StreetIndexRenderingStyle('Georgia Bold 6',
+                                                   'DejaVu 5'),
+                         StreetIndexRenderingStyle('Georgia Bold 5',
+                                                   'DejaVu 4'),
+                         StreetIndexRenderingStyle('Georgia Bold 4',
+                                                   'DejaVu 3'),
+                         StreetIndexRenderingStyle('Georgia Bold 3',
+                                                   'DejaVu 2'),
+                         StreetIndexRenderingStyle('Georgia Bold 2',
+                                                   'DejaVu 2'),
+                         StreetIndexRenderingStyle('Georgia Bold 1',
+                                                   'DejaVu 1'), ] ):
+        self._i18n             = i18n
+        self._index_categories = index_categories
+        self._rendering_styles = street_index_rendering_styles
+
+    def precompute_occupation_area(self, surface, x, y, w, h,
+                                   freedom_direction, alignment):
+        """Prepare to render the street and amenities index at the
+        given (x,y) coordinates into the provided Cairo surface. The
+        index must not be larger than the provided width and height
+        (in pixels). Nothing will be drawn on surface.
+
+        Args:
+            surface (cairo.Surface): the cairo surface to render into.
+            x (int): horizontal origin position, in pixels.
+            y (int): vertical origin position, in pixels.
+            w (int): maximum usable width for the index, in dots (Cairo unit).
+            h (int): maximum usable height for the index, in dots (Cairo unit).
+            freedom_direction (string): freedom direction, can be 'width' or
+                'height'. See _compute_columns_split for more details.
+            alignment (string): 'top' or 'bottom' for a freedom_direction
+                of 'height', 'left' or 'right' for 'width'. Tells which side to
+                stick the index to.
+
+        Returns the actual graphical StreetIndexRenderingArea defining
+        how and where the index should be rendered. Raise
+        IndexDoesNotFitError when the provided area's surface is not
+        enough to hold the index.
+        """
+        if ((freedom_direction == 'height' and
+             alignment not in ('top', 'bottom')) or
+            (freedom_direction == 'width' and
+             alignment not in ('left', 'right'))):
+            raise ValueError, 'Incompatible freedom direction and alignment!'
+
+        if not self._index_categories:
+            raise commons.IndexEmptyError
+
+        LOG.debug("Determining index area within %dx%d+%d+%d aligned %s/%s..."
+                  % (w,h,x,y, alignment, freedom_direction))
+
+        # Create a PangoCairo context for drawing to Cairo
+        ctx = cairo.Context(surface)
+        pc  = pangocairo.CairoContext(ctx)
+
+        # Iterate over the rendering_styles until we find a suitable layout
+        rendering_style = None
+        for rs in self._rendering_styles:
+            LOG.debug("Trying index fit using %s..." % rs)
+            try:
+                n_cols, min_dimension \
+                    = self._compute_columns_split(pc, rs, w, h,
+                                                  freedom_direction)
+
+                # Great: index did fit OK !
+                rendering_style = rs
+                break
+
+            except commons.IndexDoesNotFitError:
+                # Index did not fit => try smaller...
+                LOG.debug("Index %s too large: should try a smaller one."
+                        % rs)
+                continue
+
+        # Index really did not fit with any of the rendering styles ?
+        if not rendering_style:
+            raise commons.IndexDoesNotFitError("Index does not fit in area")
+
+        # Realign at bottom/top left/right
+        if freedom_direction == 'height':
+            index_width  = w
+            index_height = min_dimension
+        elif freedom_direction == 'width':
+            index_width  = min_dimension
+            index_height = h
+
+        base_offset_x = 0
+        base_offset_y = 0
+        if alignment == 'bottom':
+            base_offset_y = h - index_height
+        if alignment == 'right':
+            base_offset_x = w - index_width
+
+        area = StreetIndexRenderingArea(rendering_style,
+                                        x+base_offset_x, y+base_offset_y,
+                                        index_width, index_height, n_cols)
+        LOG.debug("Will be able to render index in %s" % area)
+        return area
+
+
+    def render(self, ctx, rendering_area, dpi = UTILS.PT_PER_INCH):
+        """
+        Render the street and amenities index at the given (x,y)
+        coordinates into the provided Cairo surface. The index must
+        not be larger than the provided surface (use
+        precompute_occupation_area() to adjust it).
+
+        Args:
+            ctx (cairo.Context): the cairo context to use for the rendering.
+            rendering_area (StreetIndexRenderingArea): the result from
+                precompute_occupation_area().
+            dpi (number): resolution of the target device.
+        """
+
+        if not self._index_categories:
+            raise commons.IndexEmptyError
+
+        LOG.debug("Rendering the street index within %s at %sdpi..."
+                  % (rendering_area, dpi))
+
+        ##
+        ## In the following, the algorithm only manipulates values
+        ## expressed in 'pt'. Only the drawing-related functions will
+        ## translate them to cairo units
+        ##
+
+        ctx.save()
+        ctx.move_to(UTILS.convert_pt_to_dots(rendering_area.x, dpi),
+                    UTILS.convert_pt_to_dots(rendering_area.y, dpi))
+
+        # Create a PangoCairo context for drawing to Cairo
+        pc = pangocairo.CairoContext(ctx)
+
+        header_fd = pango.FontDescription(
+            rendering_area.rendering_style.header_font_spec)
+        label_fd  = pango.FontDescription(
+            rendering_area.rendering_style.label_font_spec)
+
+        header_layout, header_fascent, header_fheight, header_em = \
+                self._create_layout_with_font(pc, header_fd)
+        label_layout, label_fascent, label_fheight, label_em = \
+                self._create_layout_with_font(pc, label_fd)
+
+        #print "RENDER", header_layout, header_fascent, header_fheight, 
header_em
+        #print "RENDER", label_layout, label_fascent, label_fheight, label_em
+
+        # By OCitysmap's convention, the default resolution is 72 dpi,
+        # which maps to the default pangocairo resolution (96 dpi
+        # according to pangocairo docs). If we want to render with
+        # another resolution (different from 72), we have to scale the
+        # pangocairo resolution accordingly:
+        pangocairo.context_set_resolution(label_layout.get_context(),
+                                          96.*dpi/UTILS.PT_PER_INCH)
+        pangocairo.context_set_resolution(header_layout.get_context(),
+                                          96.*dpi/UTILS.PT_PER_INCH)
+        # All this is because we want pango to have the exact same
+        # behavior as with the default 72dpi resolution. If we instead
+        # decided to call cairo::scale, then pango might choose
+        # different font metrics which don't fit in the prepared
+        # layout anymore...
+
+        margin = label_em
+        column_width = int(rendering_area.w / rendering_area.n_cols)
+
+        label_layout.set_width(int(UTILS.convert_pt_to_dots(
+                    (column_width - margin) * pango.SCALE, dpi)))
+        header_layout.set_width(int(UTILS.convert_pt_to_dots(
+                    (column_width - margin) * pango.SCALE, dpi)))
+
+        if not self._i18n.isrtl():
+            offset_x = margin/2.
+            delta_x  = column_width
+        else:
+            offset_x = rendering_area.w - column_width + margin/2.
+            delta_x  = - column_width
+
+        actual_n_cols = 1
+        offset_y = margin/2.
+        for category in self._index_categories:
+            if ( offset_y + header_fheight + label_fheight
+                 + margin/2. > rendering_area.h ):
+                offset_y       = margin/2.
+                offset_x      += delta_x
+                actual_n_cols += 1
+
+            category.draw(self._i18n.isrtl(), ctx, pc, header_layout,
+                          UTILS.convert_pt_to_dots(header_fascent, dpi),
+                          UTILS.convert_pt_to_dots(header_fheight, dpi),
+                          UTILS.convert_pt_to_dots(rendering_area.x
+                                                   + offset_x, dpi),
+                          UTILS.convert_pt_to_dots(rendering_area.y
+                                                   + offset_y
+                                                   + header_fascent, dpi))
+
+            offset_y += header_fheight
+
+            for street in category.items:
+                if ( offset_y + label_fheight + margin/2.
+                     > rendering_area.h ):
+                    offset_y       = margin/2.
+                    offset_x      += delta_x
+                    actual_n_cols += 1
+
+                street.draw(self._i18n.isrtl(), ctx, pc, label_layout,
+                            UTILS.convert_pt_to_dots(label_fascent, dpi),
+                            UTILS.convert_pt_to_dots(label_fheight, dpi),
+                            UTILS.convert_pt_to_dots(rendering_area.x
+                                                     + offset_x, dpi),
+                            UTILS.convert_pt_to_dots(rendering_area.y
+                                                     + offset_y
+                                                     + label_fascent, dpi))
+
+                offset_y += label_fheight
+
+        # Restore original context
+        ctx.restore()
+
+        # Simple verification...
+        if actual_n_cols < rendering_area.n_cols:
+            LOG.warning("Rounding/security margin lost some space (%d actual 
cols vs. allocated %d" % (actual_n_cols, rendering_area.n_cols))
+        assert actual_n_cols <= rendering_area.n_cols
+
+
+    def _create_layout_with_font(self, pc, font_desc):
+        layout = pc.create_layout()
+        layout.set_font_description(font_desc)
+        font = layout.get_context().load_font(font_desc)
+        font_metric = font.get_metrics()
+
+        fascent = float(font_metric.get_ascent()) / pango.SCALE
+        fheight = float((font_metric.get_ascent() + font_metric.get_descent())
+                        / pango.SCALE)
+        em = float(font_metric.get_approximate_char_width()) / pango.SCALE
+
+        return layout, fascent, fheight, em
+
+
+    def _compute_lines_occupation(self, pc, font_desc, n_em_padding,
+                                  text_lines):
+        """Compute the visual dimension parameters of the initial long column
+        for the given text lines with the given font.
+
+        Args:
+            pc (pangocairo.CairoContext): the PangoCairo context.
+            font_desc (pango.FontDescription): Pango font description,
+                representing the used font at a given size.
+            n_em_padding (int): number of extra em space to account for.
+            text_lines (list): the list of text labels.
+
+        Returns a dictionnary with the following key,value pairs:
+            column_width: the computed column width (pixel size of the longest
+                label).
+            column_height: the total height of the column.
+            fascent: scaled font ascent.
+            fheight: scaled font height.
+        """
+
+        layout, fascent, fheight, em = self._create_layout_with_font(pc,
+                                                                     font_desc)
+        #print "PREPARE", layout, fascent, fheight, em
+
+        width = max(map(lambda x: self._label_width(layout, x), text_lines))
+        # Save some extra space horizontally
+        width += n_em_padding * em
+
+        height = fheight * len(text_lines)
+
+        return {'column_width': width, 'column_height': height,
+                'fascent': fascent, 'fheight': fheight, 'em': em}
+
+
+    def _label_width(self, layout, label):
+        layout.set_text(label)
+        return float(layout.get_size()[0]) / pango.SCALE
+
+    def _compute_column_occupation(self, pc, rendering_style):
+        """Returns the size of the tall column with all headers, labels and
+        squares for the given font sizes.
+
+        Args:
+            pc (pangocairo.CairoContext): the PangoCairo context.
+            rendering_style (StreetIndexRenderingStyle): how to render the
+                headers and labels.
+
+        Return a tuple (width of tall column, height of tall column,
+                        vertical margin to reserve after each small column).
+        """
+
+        header_fd = pango.FontDescription(rendering_style.header_font_spec)
+        label_fd  = pango.FontDescription(rendering_style.label_font_spec)
+
+        # Account for maximum square width (at worst " " + "Z99-Z99")
+        label_block = self._compute_lines_occupation(pc, label_fd, 1+7,
+                reduce(lambda x,y: x+y.get_all_item_labels(),
+                       self._index_categories, []))
+
+        # Reserve a small margin around the category headers
+        headers_block = self._compute_lines_occupation(pc, header_fd, 2,
+                [x.name for x in self._index_categories])
+
+        column_width = max(label_block['column_width'],
+                           headers_block['column_width'])
+        column_height = label_block['column_height'] + \
+                        headers_block['column_height']
+
+        # We make sure there will be enough space for a header and a
+        # label at the bottom of each column plus an additional
+        # vertical margin (arbitrary set to 1em, see render())
+        vertical_extra = ( label_block['fheight'] + headers_block['fheight']
+                           + label_block['em'] )
+        return column_width, column_height, vertical_extra
+
+
+    def _compute_columns_split(self, pc, rendering_style,
+                               zone_width_dots, zone_height_dots,
+                               freedom_direction):
+        """Computes the columns split for this index. From the one tall column
+        width and height it finds the number of columns fitting on the zone
+        dedicated to the index on the Cairo surface.
+
+        If the columns split does not fit on the index zone,
+        commons.IndexDoesNotFitError is raised.
+
+        Args:
+            pc (pangocairo.CairoContext): the PangoCairo context.
+            rendering_style (StreetIndexRenderingStyle): how to render the
+                headers and labels.
+            zone_width_dots (float): maximum width of the Cairo zone dedicated
+                to the index.
+            zone_height_dots (float): maximum height of the Cairo zone
+                dedicated to the index.
+            freedom_direction (string): the zone dimension that is flexible for
+                rendering this index, can be 'width' or 'height'. If the
+                streets don't fill the zone dedicated to the index, we need to
+                try with a zone smaller in the freedom_direction.
+
+        Returns a tuple (number of columns that will be in the index,
+                         the new value for the flexible dimension).
+        """
+
+        tall_width, tall_height, vertical_extra = \
+                self._compute_column_occupation(pc, rendering_style)
+
+        if zone_width_dots < tall_width:
+            raise commons.IndexDoesNotFitError
+
+        if freedom_direction == 'height':
+            n_cols = math.floor(zone_width_dots / float(tall_width))
+            if n_cols <= 0:
+                raise commons.IndexDoesNotFitError
+
+            min_required_height \
+                = math.ceil(float(tall_height + n_cols*vertical_extra)
+                            / n_cols)
+
+            LOG.debug("min req H %f vs. allocatable H %f"
+                      % (min_required_height, zone_height_dots))
+
+            if min_required_height > zone_height_dots:
+                raise commons.IndexDoesNotFitError
+
+            return int(n_cols), min_required_height
+
+        elif freedom_direction == 'width':
+            n_cols = math.ceil(float(tall_height) / zone_height_dots)
+            extra = n_cols * vertical_extra
+            min_required_width = n_cols * tall_width
+
+            if ( (min_required_width > zone_width_dots)
+                 or (tall_height + extra > n_cols * zone_height_dots) ):
+                raise commons.IndexDoesNotFitError
+
+            return int(n_cols), min_required_width
+
+        raise ValueError, 'Invalid freedom direction!'
+
+
+if __name__ == '__main__':
+    import random
+    import string
+
+    import commons
+
+    logging.basicConfig(level=logging.DEBUG)
+
+    width = 72*21./2.54
+    height = .75 * 72*29.7/2.54
+
+    random.seed(42)
+
+    bbox = ocitysmap.coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699)
+
+    surface = cairo.PDFSurface('/tmp/myindex_render.pdf', width, height)
+
+    def rnd_str(max_len, letters = string.letters):
+        return ''.join(random.choice(letters)
+                       for i in xrange(random.randint(1, max_len)))
+
+    class i18nMock:
+        def __init__(self, rtl):
+            self.rtl = rtl
+        def isrtl(self):
+            return self.rtl
+
+    streets = []
+    for i in ['A', 'B', # 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 
'M',
+              'N', 'O', # 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 
'Z',
+              'Schools', 'Public buildings']:
+        items = []
+        for label, location_str in [(rnd_str(10).capitalize(),
+                                     '%s%d-%s%d' \
+                                         % (rnd_str(2,
+                                                    string.ascii_uppercase),
+                                            random.randint(1,19),
+                                            rnd_str(2,
+                                                    string.ascii_uppercase),
+                                            random.randint(1,19),
+                                            ))]*4:
+            item              = commons.IndexItem(label, None, None)
+            item.location_str = location_str
+            items.append(item)
+        streets.append(commons.IndexCategory(i, items))
+
+    index = StreetIndexRenderer(i18nMock(False), streets)
+
+    def _render(freedom_dimension, alignment):
+        x,y,w,h = 50, 50, width-100, height-100
+
+        # Draw constraining rectangle
+        ctx = cairo.Context(surface)
+
+        ctx.save()
+        ctx.set_source_rgb(.2,0,0)
+        ctx.rectangle(x,y,w,h)
+        ctx.stroke()
+
+        # Precompute index area
+        rendering_area = index.precompute_occupation_area(surface, x,y,w,h,
+                                                          freedom_dimension,
+                                                          alignment)
+
+        # Draw a green background for the precomputed area
+        ctx.set_source_rgba(0,1,0,.5)
+        ctx.rectangle(rendering_area.x, rendering_area.y,
+                      rendering_area.w, rendering_area.h)
+        ctx.fill()
+        ctx.restore()
+
+        # Render the index
+        index.render(ctx, rendering_area)
+
+
+    _render('height', 'top')
+    surface.show_page()
+    _render('height', 'bottom')
+    surface.show_page()
+    _render('width', 'left')
+    surface.show_page()
+    _render('width', 'right')
+    surface.show_page()
+
+    index = StreetIndexRenderer(i18nMock(True), streets)
+    _render('height', 'top')
+    surface.show_page()
+    _render('height', 'bottom')
+    surface.show_page()
+    _render('width', 'left')
+    surface.show_page()
+    _render('width', 'right')
+
+    surface.finish()
+    print "Generated /tmp/myindex_render.pdf"
diff --git a/ocitysmap/layoutlib/__init__.py b/ocitysmap/layoutlib/__init__.py
new file mode 100644
index 0000000..83ed70e
--- /dev/null
+++ b/ocitysmap/layoutlib/__init__.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Portrait paper sizes in milimeters
+PAPER_SIZES = [('A5', 148, 210),
+               ('A4', 210, 297),
+               ('A3', 297, 420),
+               ('A2', 420, 594),
+               ('A1', 594, 841),
+               ('A0', 841, 1189),
+
+               ('US letter', 216, 279),
+
+               ('100x75cm', 750, 1000),
+               ('80x60cm', 600, 800),
+               ('60x45cm', 450, 600),
+               ('40x30cm', 300, 400),
+
+               ('60x60cm', 600, 600),
+               ('50x50cm', 500, 500),
+               ('40x40cm', 400, 400),
+
+               ('Best fit', None, None),
+               ]
diff --git a/ocitysmap/layoutlib/abstract_renderer.py 
b/ocitysmap/layoutlib/abstract_renderer.py
new file mode 100644
index 0000000..3824d25
--- /dev/null
+++ b/ocitysmap/layoutlib/abstract_renderer.py
@@ -0,0 +1,281 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2012  David Decotigny
+# Copyright (C) 2012  Frédéric Lehobey
+# Copyright (C) 2012  Pierre Mauduit
+# Copyright (C) 2012  David Mentré
+# Copyright (C) 2012  Maxime Petazzoni
+# Copyright (C) 2012  Thomas Petazzoni
+# Copyright (C) 2012  Gaël Utard
+# Copyright (C) 2012  Étienne Loks
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import logging
+import mapnik
+assert mapnik.mapnik_version >= 200100, \
+    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
+    "for more details." % mapnik.mapnik_version_string()
+import math
+import pango
+import os
+import re
+import shapely.wkt
+import sys
+
+import commons
+from ocitysmap.maplib.map_canvas import MapCanvas
+from ocitysmap.maplib.grid import Grid
+from ocitysmap import draw_utils, maplib
+
+LOG = logging.getLogger('ocitysmap')
+
+
+class Renderer:
+    """
+    The job of an OCitySMap layout renderer is to lay out the resulting map and
+    render it from a given rendering configuration.
+    """
+    name = 'abstract'
+    description = 'The abstract interface of a renderer'
+
+    # The PRINT_SAFE_MARGIN_PT is a small margin we leave on all page borders
+    # to ease printing as printers often eat up margins with misaligned paper,
+    # etc.
+    PRINT_SAFE_MARGIN_PT = 15
+
+    GRID_LEGEND_MARGIN_RATIO = .02
+
+    # The DEFAULT_KM_IN_MM represents the minimum acceptable mapnik scale
+    # 12000 ensures that the zoom level will be 16 or higher
+    # see entities.xml.inc file from osm style sheet
+    DEFAULT_SCALE = 12000
+
+    def __init__(self, db, rc, tmpdir, dpi):
+        """
+        Create the renderer.
+
+        Args:
+           rc (RenderingConfiguration): rendering parameters.
+           tmpdir (os.path): Path to a temp dir that can hold temp files.
+           street_index (StreetIndex): None or the street index object.
+        """
+        # Note: street_index may be None
+        self.db           = db
+        self.rc           = rc
+        self.tmpdir       = tmpdir
+        self.grid         = None # The implementation is in charge of it
+
+        self.paper_width_pt = \
+                commons.convert_mm_to_pt(self.rc.paper_width_mm)
+        self.paper_height_pt = \
+                commons.convert_mm_to_pt(self.rc.paper_height_mm)
+
+    @staticmethod
+    def _get_osm_logo(ctx, height):
+        """
+        Read the OSM logo file and rescale it to fit within height.
+
+        Args:
+           ctx (cairo.Context): The cairo context to use to draw.
+           height (number): final height of the logo (cairo units).
+
+        Return a tuple (cairo group object for the logo, logo width in
+                        cairo units).
+        """
+        # TODO: read vector logo
+        logo_path = os.path.abspath(os.path.join(
+            os.path.dirname(__file__), '..', '..', 'images', 'osm-logo.png'))
+        if not os.path.exists(logo_path):
+            logo_path = os.path.join(
+                sys.exec_prefix, 'share', 'images', 'ocitysmap',
+                'osm-logo.png')
+
+        try:
+            with open(logo_path, 'rb') as f:
+                png = cairo.ImageSurface.create_from_png(f)
+                LOG.debug('Using copyright logo: %s.' % logo_path)
+        except IOError:
+            LOG.warning('Cannot open logo from %s.' % logo_path)
+            return None, None
+
+        ctx.push_group()
+        ctx.save()
+        ctx.move_to(0, 0)
+        factor = height / png.get_height()
+        ctx.scale(factor, factor)
+        ctx.set_source_surface(png)
+        ctx.paint()
+        ctx.restore()
+        return ctx.pop_group(), png.get_width()*factor
+
+    @staticmethod
+    def _draw_labels(ctx, map_grid,
+                     map_area_width_dots, map_area_height_dots,
+                     grid_legend_margin_dots):
+        """
+        Draw the Grid labels at current position.
+
+        Args:
+           ctx (cairo.Context): The cairo context to use to draw.
+           map_grid (Grid): the grid objects whose labels we want to draw.
+           map_area_width_dots/map_area_height_dots (numbers): size of the
+              map (cairo units).
+           grid_legend_margin_dots (number): margin between border of
+              map and grid labels (cairo units).
+        """
+        ctx.save()
+
+        step_horiz = map_area_width_dots / map_grid.horiz_count
+        last_horiz_portion = math.modf(map_grid.horiz_count)[0]
+
+        step_vert = map_area_height_dots / map_grid.vert_count
+        last_vert_portion = math.modf(map_grid.vert_count)[0]
+
+        ctx.set_font_size(min(0.75 * grid_legend_margin_dots,
+                              0.5 * step_horiz))
+
+        for i, label in enumerate(map_grid.horizontal_labels):
+            x = i * step_horiz
+
+            if i < len(map_grid.horizontal_labels) - 1:
+                x += step_horiz/2.0
+            elif last_horiz_portion >= 0.3:
+                x += step_horiz * last_horiz_portion/2.0
+            else:
+                continue
+
+            draw_utils.draw_simpletext_center(ctx, label,
+                                         x, grid_legend_margin_dots/2.0)
+            draw_utils.draw_simpletext_center(ctx, label,
+                                         x, map_area_height_dots -
+                                         grid_legend_margin_dots/2.0)
+
+        for i, label in enumerate(map_grid.vertical_labels):
+            y = i * step_vert
+
+            if i < len(map_grid.vertical_labels) - 1:
+                y += step_vert/2.0
+            elif last_vert_portion >= 0.3:
+                y += step_vert * last_vert_portion/2.0
+            else:
+                continue
+
+            draw_utils.draw_simpletext_center(ctx, label,
+                                         grid_legend_margin_dots/2.0, y)
+            draw_utils.draw_simpletext_center(ctx, label,
+                                         map_area_width_dots -
+                                         grid_legend_margin_dots/2.0, y)
+
+        ctx.restore()
+
+    def _create_map_canvas(self, width, height, dpi,
+                           draw_contour_shade = True):
+        """
+        Create a new MapCanvas object.
+
+        Args:
+           graphical_ratio (float): ratio W/H of the area to render into.
+           draw_contour_shade (bool): whether to draw a shade around
+               the area of interest or not.
+
+        Return the MapCanvas object or raise ValueError.
+        """
+
+        # Prepare the map canvas
+        canvas = MapCanvas(self.rc.stylesheet,
+                           self.rc.bounding_box,
+                           width, height, dpi)
+
+        if draw_contour_shade:
+            # Area to keep visible
+            interior = shapely.wkt.loads(self.rc.polygon_wkt)
+
+            # Surroundings to gray-out
+            bounding_box \
+                = canvas.get_actual_bounding_box().create_expanded(0.05, 0.05)
+            exterior = shapely.wkt.loads(bounding_box.as_wkt())
+
+            # Determine the shade WKT
+            shade_wkt = exterior.difference(interior).wkt
+
+            # Prepare the shade SHP
+            shade_shape = maplib.shapes.PolyShapeFile(
+                canvas.get_actual_bounding_box(),
+                os.path.join(self.tmpdir, 'shade.shp'),
+                'shade')
+            shade_shape.add_shade_from_wkt(shade_wkt)
+
+            # Add the shade SHP to the map
+            canvas.add_shape_file(shade_shape,
+                                  self.rc.stylesheet.shade_color,
+                                  self.rc.stylesheet.shade_alpha,
+                                  self.rc.stylesheet.grid_line_width)
+
+        return canvas
+
+    def _create_grid(self, canvas):
+        """
+        Create a new Grid object for the given MapCanvas.
+
+        Args:
+           canvas (MapCanvas): Map Canvas (see _create_map_canvas).
+
+        Return a new Grid object.
+        """
+        # Prepare the grid SHP
+        map_grid = Grid(canvas.get_actual_bounding_box(), 
canvas.get_actual_scale(), self.rc.i18n.isrtl())
+        grid_shape = map_grid.generate_shape_file(
+            os.path.join(self.tmpdir, 'grid.shp'))
+
+        # Add the grid SHP to the map
+        canvas.add_shape_file(grid_shape,
+                              self.rc.stylesheet.grid_line_color,
+                              self.rc.stylesheet.grid_line_alpha,
+                              self.rc.stylesheet.grid_line_width)
+
+        return map_grid
+
+    # The next two methods are to be overloaded by the actual renderer.
+    def render(self, cairo_surface, dpi):
+        """Renders the map, the index and all other visual map features on the
+        given Cairo surface.
+
+        Args:
+            cairo_surface (Cairo.Surface): the destination Cairo device.
+            dpi (int): dots per inch of the device.
+        """
+        raise NotImplementedError
+
+    @staticmethod
+    def get_compatible_output_formats():
+        return [ "png", "svgz", "pdf", "csv" ]
+
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box, scale):
+        """Returns a list of the compatible paper sizes for the given bounding
+        box. The list is sorted, smaller papers first, and a "custom" paper
+        matching the dimensions of the bounding box is added at the end.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map geographic bounding box.
+            scale (int): minimum mapnik scale of the map.
+
+        Returns a list of tuples (paper name, width in mm, height in
+        mm, portrait_ok, landscape_ok, is_default). Paper sizes are
+        represented in portrait mode.
+        """
+        raise NotImplementedError
diff --git a/ocitysmap/layoutlib/commons.py b/ocitysmap/layoutlib/commons.py
new file mode 100644
index 0000000..75959ad
--- /dev/null
+++ b/ocitysmap/layoutlib/commons.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# PT/metrics conversion routines
+PT_PER_INCH = 72.0
+
+def convert_pt_to_dots(pt, dpi = PT_PER_INCH):
+    return float(pt * dpi) / PT_PER_INCH
+
+def convert_mm_to_pt(mm):
+    return ((mm/10.0) / 2.54) * 72
+
+def convert_pt_to_mm(pt):
+    return (float(pt) * 10.0 * 2.54) / 72
diff --git a/ocitysmap/layoutlib/multi_page_renderer.py 
b/ocitysmap/layoutlib/multi_page_renderer.py
new file mode 100644
index 0000000..e22613a
--- /dev/null
+++ b/ocitysmap/layoutlib/multi_page_renderer.py
@@ -0,0 +1,783 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2012  David Mentré
+# Copyright (C) 2012  Thomas Petazzoni
+# Copyright (C) 2012  Gaël Utard
+# Copyright (C) 2012  Étienne Loks
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import datetime
+from itertools import groupby
+import locale
+import logging
+import mapnik
+assert mapnik.mapnik_version >= 200100, \
+    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
+    "for more details." % mapnik.mapnik_version_string()
+import math
+import os
+import pangocairo
+import pango
+import shapely.wkt
+import sys
+import tempfile
+
+import ocitysmap
+import coords
+import commons
+from abstract_renderer import Renderer
+from indexlib.commons import IndexCategory
+from indexlib.indexer import StreetIndex
+from indexlib.multi_page_renderer import MultiPageStreetIndexRenderer
+from ocitysmap import draw_utils, maplib
+from ocitysmap.maplib.map_canvas import MapCanvas
+from ocitysmap.maplib.grid import Grid
+from ocitysmap.maplib.overview_grid import OverviewGrid
+
+LOG = logging.getLogger('ocitysmap')
+
+
+class MultiPageRenderer(Renderer):
+    """
+    This Renderer creates a multi-pages map, with all the classic overlayed
+    features and no index page.
+    """
+
+    name = 'multi_page'
+    description = 'A multi-page layout.'
+    multipages = True
+
+    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
+        Renderer.__init__(self, db, rc, tmpdir, dpi)
+
+        self._grid_legend_margin_pt = \
+            min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt,
+                Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt)
+
+        # Compute the usable area per page
+        self._usable_area_width_pt = (self.paper_width_pt -
+                                      (2 * Renderer.PRINT_SAFE_MARGIN_PT))
+        self._usable_area_height_pt = (self.paper_height_pt -
+                                       (2 * Renderer.PRINT_SAFE_MARGIN_PT))
+
+        scale_denom = Renderer.DEFAULT_SCALE
+
+        # the mapnik scale depends on the latitude. However we are
+        # always using Mapnik conversion functions (lat,lon <->
+        # mercator_meters) so we don't need to take into account
+        # latitude in following computations
+
+        # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi
+        scale_denom *= float(72) / 90
+
+        GRAYED_MARGIN_MM  = 10
+        OVERLAP_MARGIN_MM = 20
+
+        # Debug: show original bounding box as JS code
+        # print self.rc.bounding_box.as_javascript("original", "#00ff00")
+
+        # Convert the original Bounding box into Mercator meters
+        self._proj = mapnik.Projection(coords._MAPNIK_PROJECTION)
+        orig_envelope = self._project_envelope(self.rc.bounding_box)
+
+        # Extend the bounding box to take into account the lost outter
+        # margin
+        off_x  = orig_envelope.minx - (GRAYED_MARGIN_MM * scale_denom) / 1000
+        off_y  = orig_envelope.miny - (GRAYED_MARGIN_MM * scale_denom) / 1000
+        width  = orig_envelope.width() + (2 * GRAYED_MARGIN_MM * scale_denom) 
/ 1000
+        height = orig_envelope.height() + (2 * GRAYED_MARGIN_MM * scale_denom) 
/ 1000
+
+        # Calculate the total width and height of paper needed to
+        # render the geographical area at the current scale.
+        total_width_pt   = commons.convert_mm_to_pt(float(width) * 1000 / 
scale_denom)
+        total_height_pt  = commons.convert_mm_to_pt(float(height) * 1000 / 
scale_denom)
+        self.grayed_margin_pt = commons.convert_mm_to_pt(GRAYED_MARGIN_MM)
+        overlap_margin_pt = commons.convert_mm_to_pt(OVERLAP_MARGIN_MM)
+
+        # Calculate the number of pages needed in both directions
+        if total_width_pt < self._usable_area_width_pt:
+            nb_pages_width = 1
+        else:
+            nb_pages_width = \
+                (float(total_width_pt - self._usable_area_width_pt) / \
+                     (self._usable_area_width_pt - overlap_margin_pt)) + 1
+
+        if total_height_pt < self._usable_area_height_pt:
+            nb_pages_height = 1
+        else:
+            nb_pages_height = \
+                (float(total_height_pt - self._usable_area_height_pt) / \
+                     (self._usable_area_height_pt - overlap_margin_pt)) + 1
+
+        # Round up the number of pages needed so that we have integer
+        # number of pages
+        self.nb_pages_width = int(math.ceil(nb_pages_width))
+        self.nb_pages_height = int(math.ceil(nb_pages_height))
+
+        # Calculate the entire paper area available
+        total_width_pt_after_extension = self._usable_area_width_pt + \
+            (self._usable_area_width_pt - overlap_margin_pt) * 
(self.nb_pages_width - 1)
+        total_height_pt_after_extension = self._usable_area_height_pt + \
+            (self._usable_area_height_pt - overlap_margin_pt) * 
(self.nb_pages_height - 1)
+
+        # Convert this paper area available in the number of Mercator
+        # meters that can be rendered on the map
+        total_width_merc = \
+            commons.convert_pt_to_mm(total_width_pt_after_extension) * 
scale_denom / 1000
+        total_height_merc = \
+            commons.convert_pt_to_mm(total_height_pt_after_extension) * 
scale_denom / 1000
+
+        # Extend the geographical boundaries so that we completely
+        # fill the available paper size. We are careful to extend the
+        # boundaries evenly on all directions (so the center of the
+        # previous boundaries remain the same as the new one)
+        off_x -= (total_width_merc - width) / 2
+        width = total_width_merc
+        off_y -= (total_height_merc - height) / 2
+        height = total_height_merc
+
+        # Calculate what is the final global bounding box that we will render
+        envelope = mapnik.Box2d(off_x, off_y, off_x + width, off_y + height)
+        self._geo_bbox = self._inverse_envelope(envelope)
+
+        # Debug: show transformed bounding box as JS code
+        # print self._geo_bbox.as_javascript("extended", "#0f0f0f")
+
+        # Convert the usable area on each sheet of paper into the
+        # amount of Mercator meters we can render in this area.
+        usable_area_merc_m_width  = 
commons.convert_pt_to_mm(self._usable_area_width_pt) * scale_denom / 1000
+        usable_area_merc_m_height = 
commons.convert_pt_to_mm(self._usable_area_height_pt) * scale_denom / 1000
+        grayed_margin_merc_m      = (GRAYED_MARGIN_MM * scale_denom) / 1000
+        overlap_margin_merc_m     = (OVERLAP_MARGIN_MM * scale_denom) / 1000
+
+        # Calculate all the bounding boxes that correspond to the
+        # geographical area that will be rendered on each sheet of
+        # paper.
+        area_polygon = shapely.wkt.loads(self.rc.polygon_wkt)
+        bboxes = []
+        self.page_disposition, map_number = {}, 0
+        for j in reversed(range(0, self.nb_pages_height)):
+            col = self.nb_pages_height - j - 1
+            self.page_disposition[col] = []
+            for i in range(0, self.nb_pages_width):
+                cur_x = off_x + i * (usable_area_merc_m_width - 
overlap_margin_merc_m)
+                cur_y = off_y + j * (usable_area_merc_m_height - 
overlap_margin_merc_m)
+                envelope = mapnik.Box2d(cur_x, cur_y,
+                                        cur_x+usable_area_merc_m_width,
+                                        cur_y+usable_area_merc_m_height)
+
+                envelope_inner = mapnik.Box2d(cur_x + grayed_margin_merc_m,
+                                              cur_y + grayed_margin_merc_m,
+                                              cur_x + usable_area_merc_m_width 
 - grayed_margin_merc_m,
+                                              cur_y + 
usable_area_merc_m_height - grayed_margin_merc_m)
+                inner_bb = self._inverse_envelope(envelope_inner)
+                if not area_polygon.disjoint(shapely.wkt.loads(
+                                                inner_bb.as_wkt())):
+                    self.page_disposition[col].append(map_number)
+                    map_number += 1
+                    bboxes.append((self._inverse_envelope(envelope),
+                                   inner_bb))
+                else:
+                    self.page_disposition[col].append(None)
+        # Debug: show per-page bounding boxes as JS code
+        # for i, (bb, bb_inner) in enumerate(bboxes):
+        #    print bb.as_javascript(name="p%d" % i)
+
+        self.pages = []
+
+        # Create an overview map
+
+        overview_bb = self._geo_bbox.create_expanded(0.001, 0.001)
+        # Create the overview grid
+        self.overview_grid = OverviewGrid(overview_bb,
+                     [bb_inner for bb, bb_inner in bboxes], 
self.rc.i18n.isrtl())
+
+        grid_shape = self.overview_grid.generate_shape_file(
+                    os.path.join(self.tmpdir, 'grid_overview.shp'))
+
+        # Create a canvas for the overview page
+        self.overview_canvas = MapCanvas(self.rc.stylesheet,
+                               overview_bb, self._usable_area_width_pt,
+                               self._usable_area_height_pt, dpi,
+                               extend_bbox_to_ratio=True)
+
+        # Create the gray shape around the overview map
+        exterior = 
shapely.wkt.loads(self.overview_canvas.get_actual_bounding_box()\
+                                                                .as_wkt())
+        interior = shapely.wkt.loads(self.rc.polygon_wkt)
+        shade_wkt = exterior.difference(interior).wkt
+        shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
+                os.path.join(self.tmpdir, 'shape_overview.shp'),
+                             'shade-overview')
+        shade.add_shade_from_wkt(shade_wkt)
+
+        self.overview_canvas.add_shape_file(shade)
+        self.overview_canvas.add_shape_file(grid_shape,
+                                  self.rc.stylesheet.grid_line_color, 1,
+                                  self.rc.stylesheet.grid_line_width)
+
+        self.overview_canvas.render()
+
+        # Create the map canvas for each page
+        indexes = []
+        for i, (bb, bb_inner) in enumerate(bboxes):
+
+            # Create the gray shape around the map
+            exterior = shapely.wkt.loads(bb.as_wkt())
+            interior = shapely.wkt.loads(bb_inner.as_wkt())
+            shade_wkt = exterior.difference(interior).wkt
+            shade = maplib.shapes.PolyShapeFile(
+                bb, os.path.join(self.tmpdir, 'shade%d.shp' % i),
+                'shade%d' % i)
+            shade.add_shade_from_wkt(shade_wkt)
+
+
+            # Create the contour shade
+
+            # Area to keep visible
+            interior_contour = shapely.wkt.loads(self.rc.polygon_wkt)
+            # Determine the shade WKT
+            shade_contour_wkt = interior.difference(interior_contour).wkt
+            # Prepare the shade SHP
+            shade_contour = maplib.shapes.PolyShapeFile(bb,
+                os.path.join(self.tmpdir, 'shade_contour%d.shp' % i),
+                'shade_contour%d' % i)
+            shade_contour.add_shade_from_wkt(shade_contour_wkt)
+
+
+            # Create one canvas for the current page
+            map_canvas = MapCanvas(self.rc.stylesheet,
+                                   bb, self._usable_area_width_pt,
+                                   self._usable_area_height_pt, dpi,
+                                   extend_bbox_to_ratio=False)
+
+            # Create the grid
+            map_grid = Grid(bb_inner, map_canvas.get_actual_scale(), 
self.rc.i18n.isrtl())
+            grid_shape = map_grid.generate_shape_file(
+                os.path.join(self.tmpdir, 'grid%d.shp' % i))
+
+            map_canvas.add_shape_file(shade)
+            map_canvas.add_shape_file(shade_contour,
+                                  self.rc.stylesheet.shade_color_2,
+                                  self.rc.stylesheet.shade_alpha_2)
+            map_canvas.add_shape_file(grid_shape,
+                                      self.rc.stylesheet.grid_line_color,
+                                      self.rc.stylesheet.grid_line_alpha,
+                                      self.rc.stylesheet.grid_line_width)
+
+            map_canvas.render()
+            self.pages.append((map_canvas, map_grid))
+
+            # Create the index for the current page
+            inside_contour_wkt = interior_contour.intersection(interior).wkt
+            index = StreetIndex(self.db,
+                                inside_contour_wkt,
+                                self.rc.i18n, page_number=(i + 4))
+
+            index.apply_grid(map_grid)
+            indexes.append(index)
+
+        # Merge all indexes
+        self.index_categories = self._merge_page_indexes(indexes)
+
+        # Prepare the small map for the front page
+        self._front_page_map = self._prepare_front_page_map(dpi)
+
+    def _merge_page_indexes(self, indexes):
+        # First, we split street categories and "other" categories,
+        # because we sort them and we don't want to have the "other"
+        # categories intermixed with the street categories. This
+        # sorting is required for the groupby Python operator to work
+        # properly.
+        all_categories_streets = []
+        all_categories_others  = []
+        for page_number, idx in enumerate(indexes):
+            for cat in idx.categories:
+                # Split in two lists depending on the category type
+                # (street or other)
+                if cat.is_street:
+                    all_categories_streets.append(cat)
+                else:
+                    all_categories_others.append(cat)
+
+        all_categories_streets_merged = \
+            self._merge_index_same_categories(all_categories_streets, 
is_street=True)
+        all_categories_others_merged = \
+            self._merge_index_same_categories(all_categories_others, 
is_street=False)
+
+        all_categories_merged = \
+            all_categories_streets_merged + all_categories_others_merged
+
+        return all_categories_merged
+
+    def _merge_index_same_categories(self, categories, is_street=True):
+        # Sort by categories. Now we may have several consecutive
+        # categories with the same name (i.e category for letter 'A'
+        # from page 1, category for letter 'A' from page 3).
+        categories.sort(key=lambda s:s.name)
+
+        categories_merged = []
+        for category_name,grouped_categories in groupby(categories,
+                                                        key=lambda s:s.name):
+
+            # Group the different IndexItem from categories having the
+            # same name. The groupby() function guarantees us that
+            # categories with the same name are grouped together in
+            # grouped_categories[].
+
+            grouped_items = []
+            for cat in grouped_categories:
+                grouped_items.extend(cat.items)
+
+            # Re-sort alphabetically all the IndexItem according to
+            # the street name.
+
+            prev_locale = locale.getlocale(locale.LC_COLLATE)
+            locale.setlocale(locale.LC_COLLATE, self.rc.i18n.language_code())
+            try:
+                grouped_items_sorted = \
+                    sorted(grouped_items,
+                           lambda x,y: locale.strcoll(x.label, y.label))
+            finally:
+                locale.setlocale(locale.LC_COLLATE, prev_locale)
+
+            self._blank_duplicated_names(grouped_items_sorted)
+
+            # Rebuild a IndexCategory object with the list of merged
+            # and sorted IndexItem
+            categories_merged.append(
+                IndexCategory(category_name, grouped_items_sorted, is_street))
+
+        return categories_merged
+
+    # We set the label to empty string in case of duplicated item. In
+    # multi-page renderer we won't draw the dots in that case
+    def _blank_duplicated_names(self, grouped_items_sorted):
+        prev_label = ''
+        for item in grouped_items_sorted:
+            if prev_label == item.label:
+                item.label = ''
+            else:
+                prev_label = item.label
+
+    def _project_envelope(self, bbox):
+        """Project the given bounding box into the rendering projection."""
+        envelope = mapnik.Box2d(bbox.get_top_left()[1],
+                                bbox.get_top_left()[0],
+                                bbox.get_bottom_right()[1],
+                                bbox.get_bottom_right()[0])
+        c0 = self._proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return mapnik.Box2d(c0.x, c0.y, c1.x, c1.y)
+
+    def _inverse_envelope(self, envelope):
+        """Inverse the given cartesian envelope (in 900913) back to a 4002
+        bounding box."""
+        c0 = self._proj.inverse(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.inverse(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return coords.BoundingBox(c0.y, c0.x, c1.y, c1.x)
+
+    def _prepare_front_page_map(self, dpi):
+        front_page_map_w = \
+            self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
+        front_page_map_h = \
+            (self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT) 
/ 2
+
+        # Create the nice small map
+        front_page_map = \
+            MapCanvas(self.rc.stylesheet,
+                      self.rc.bounding_box,
+                      front_page_map_w,
+                      front_page_map_h,
+                      dpi,
+                      extend_bbox_to_ratio=True)
+
+        # Add the shape that greys out everything that is outside of
+        # the administrative boundary.
+        exterior = 
shapely.wkt.loads(front_page_map.get_actual_bounding_box().as_wkt())
+        interior = shapely.wkt.loads(self.rc.polygon_wkt)
+        shade_wkt = exterior.difference(interior).wkt
+        shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
+                os.path.join(self.tmpdir, 'shape_overview_cover.shp'),
+                             'shade-overview-cover')
+        shade.add_shade_from_wkt(shade_wkt)
+        front_page_map.add_shape_file(shade)
+        front_page_map.render()
+        return front_page_map
+
+    def _render_front_page_header(self, ctx, w, h):
+        # Draw a light blue block which will contain the name of the
+        # city being rendered.
+        blue_w = w
+        blue_h = 0.3 * h
+        ctx.set_source_rgb(.80,.80,.80)
+        ctx.rectangle(0, 0, blue_w, blue_h)
+        ctx.fill()
+        draw_utils.draw_text_adjusted(ctx, self.rc.title, blue_w/2, blue_h/2,
+                 blue_w, blue_h)
+
+    def _render_front_page_map(self, ctx, dpi, w, h):
+        # We will render the map slightly below the title
+        ctx.save()
+        ctx.translate(0, 0.3 * h + Renderer.PRINT_SAFE_MARGIN_PT)
+
+        # Render the map !
+        mapnik.render(self._front_page_map.get_rendered_map(), ctx)
+        ctx.restore()
+
+    def _render_front_page_footer(self, ctx, w, h, osm_date):
+        ctx.save()
+
+        # Draw the footer
+        ctx.translate(0, 0.8 * h + 2 * Renderer.PRINT_SAFE_MARGIN_PT)
+
+        # Display a nice grey rectangle as the background of the
+        # footer
+        footer_w = w
+        footer_h = 0.2 * h - 2 * Renderer.PRINT_SAFE_MARGIN_PT
+        ctx.set_source_rgb(.80,.80,.80)
+        ctx.rectangle(0, 0, footer_w, footer_h)
+        ctx.fill()
+
+        # Draw the OpenStreetMap logo to the right of the footer
+        logo_height = footer_h / 2
+        grp, logo_width = self._get_osm_logo(ctx, logo_height)
+        if grp:
+            ctx.save()
+            ctx.translate(w - logo_width - Renderer.PRINT_SAFE_MARGIN_PT,
+                          logo_height / 2)
+            ctx.set_source(grp)
+            ctx.paint_with_alpha(0.8)
+            ctx.restore()
+
+        # Prepare the text for the left of the footer
+        today = datetime.date.today()
+        notice = \
+            _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers.\n'
+              u'http://www.maposmatic.org\n\n'
+              u'Map data © %(year)d OpenStreetMap.org '
+              u'and contributors (cc-by-sa).\n'
+              u'http://www.openstreetmap.org\n\n'
+              u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s.\n'
+              u'The map may be incomplete or inaccurate. '
+              u'You can contribute to improve this map.\n'
+              u'See http://wiki.openstreetmap.org')
+
+        # We need the correct locale to be set for strftime().
+        prev_locale = locale.getlocale(locale.LC_TIME)
+        locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code())
+        try:
+            if osm_date is None:
+                osm_date_str = _(u'unknown')
+            else:
+                osm_date_str = osm_date.strftime("%d %B %Y %H:%M")
+
+            notice = notice % {'year': today.year,
+                               'date': today.strftime("%d %B %Y"),
+                               'osmdate': osm_date_str}
+        finally:
+            locale.setlocale(locale.LC_TIME, prev_locale)
+
+        draw_utils.draw_text_adjusted(ctx, notice,
+                Renderer.PRINT_SAFE_MARGIN_PT, footer_h/2, footer_w,
+                footer_h, align=pango.ALIGN_LEFT)
+        ctx.restore()
+
+    def _render_front_page(self, ctx, cairo_surface, dpi, osm_date):
+        # Draw a nice grey rectangle covering the whole page
+        ctx.save()
+        ctx.set_source_rgb(.95,.95,.95)
+        ctx.rectangle(Renderer.PRINT_SAFE_MARGIN_PT,
+                      Renderer.PRINT_SAFE_MARGIN_PT,
+                      self._usable_area_width_pt,
+                      self._usable_area_height_pt)
+        ctx.fill()
+        ctx.restore()
+
+        # Translate into the working area, taking another
+        # PRINT_SAFE_MARGIN_PT inside the grey area.
+        ctx.save()
+        ctx.translate(2 * Renderer.PRINT_SAFE_MARGIN_PT,
+                      2 * Renderer.PRINT_SAFE_MARGIN_PT)
+        w = self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
+        h = self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
+
+        self._render_front_page_header(ctx, w, h)
+        self._render_front_page_map(ctx, dpi, w, h)
+        self._render_front_page_footer(ctx, w, h, osm_date)
+
+        ctx.restore()
+
+        cairo_surface.show_page()
+
+    def _render_blank_page(self, ctx, cairo_surface, dpi):
+        """
+        Render a blank page with a nice "intentionally blank" notice
+        """
+        ctx.save()
+        ctx.translate(
+                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
+                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
+
+        # footer notice
+        w = self._usable_area_width_pt
+        h = self._usable_area_height_pt
+        ctx.set_source_rgb(.6,.6,.6)
+        draw_utils.draw_simpletext_center(ctx, _('This page is intentionally 
left '\
+                                            'blank.'), w/2.0, 0.95*h)
+        draw_utils.render_page_number(ctx, 2,
+                                      self._usable_area_width_pt,
+                                      self._usable_area_height_pt,
+                                      self.grayed_margin_pt,
+                                      transparent_background=False)
+        cairo_surface.show_page()
+        ctx.restore()
+
+    def _render_overview_page(self, ctx, cairo_surface, dpi):
+        rendered_map = self.overview_canvas.get_rendered_map()
+        mapnik.render(rendered_map, ctx)
+
+        # draw pages numbers
+        self._draw_overview_labels(ctx, self.overview_canvas, 
self.overview_grid,
+              commons.convert_pt_to_dots(self._usable_area_width_pt),
+              commons.convert_pt_to_dots(self._usable_area_height_pt))
+        # Render the page number
+        draw_utils.render_page_number(ctx, 3,
+                                      self._usable_area_width_pt,
+                                      self._usable_area_height_pt,
+                                      self.grayed_margin_pt,
+                                      transparent_background = True)
+
+        cairo_surface.show_page()
+
+    def _draw_arrow(self, ctx, cairo_surface, number, max_digit_number,
+                    reverse_text=False):
+        arrow_edge = self.grayed_margin_pt*.6
+        ctx.save()
+        ctx.set_source_rgb(0, 0, 0)
+        ctx.translate(-arrow_edge/2, -arrow_edge*0.45)
+        ctx.line_to(0, 0)
+        ctx.line_to(0, arrow_edge)
+        ctx.line_to(arrow_edge, arrow_edge)
+        ctx.line_to(arrow_edge, 0)
+        ctx.line_to(arrow_edge/2, -arrow_edge*.25)
+        ctx.close_path()
+        ctx.fill()
+        ctx.restore()
+
+        ctx.save()
+        if reverse_text:
+            ctx.rotate(math.pi)
+        draw_utils.draw_text_adjusted(ctx, unicode(number), 0, 0, arrow_edge,
+                        arrow_edge, max_char_number=max_digit_number,
+                        text_color=(1, 1, 1, 1), width_adjust=0.85,
+                        height_adjust=0.9)
+        ctx.restore()
+
+    def _render_neighbour_arrows(self, ctx, cairo_surface, map_number,
+                                 max_digit_number):
+        nb_previous_pages = 4
+        current_line, current_col = None, None
+        for line_nb in xrange(self.nb_pages_height):
+            if map_number in self.page_disposition[line_nb]:
+                current_line = line_nb
+                current_col = self.page_disposition[line_nb].index(
+                                                             map_number)
+                break
+        if current_line == None:
+            # page not referenced
+            return
+
+        # north arrow
+        for line_nb in reversed(xrange(current_line)):
+            if self.page_disposition[line_nb][current_col] != None:
+                north_arrow = self.page_disposition[line_nb][current_col]
+                ctx.save()
+                ctx.translate(self._usable_area_width_pt/2,
+                    commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
+                self._draw_arrow(ctx, cairo_surface,
+                              north_arrow + nb_previous_pages, 
max_digit_number)
+                ctx.restore()
+                break
+
+        # south arrow
+        for line_nb in xrange(current_line + 1, self.nb_pages_height):
+            if self.page_disposition[line_nb][current_col] != None:
+                south_arrow = self.page_disposition[line_nb][current_col]
+                ctx.save()
+                ctx.translate(self._usable_area_width_pt/2,
+                     self._usable_area_height_pt \
+                      - commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
+                ctx.rotate(math.pi)
+                self._draw_arrow(ctx, cairo_surface,
+                      south_arrow + nb_previous_pages, max_digit_number,
+                      reverse_text=True)
+                ctx.restore()
+                break
+
+        # west arrow
+        for col_nb in reversed(xrange(0, current_col)):
+            if self.page_disposition[current_line][col_nb] != None:
+                west_arrow = self.page_disposition[current_line][col_nb]
+                ctx.save()
+                ctx.translate(
+                    commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
+                    self._usable_area_height_pt/2)
+                ctx.rotate(-math.pi/2)
+                self._draw_arrow(ctx, cairo_surface,
+                               west_arrow + nb_previous_pages, 
max_digit_number)
+                ctx.restore()
+                break
+
+        # east arrow
+        for col_nb in xrange(current_col + 1, self.nb_pages_width):
+            if self.page_disposition[current_line][col_nb] != None:
+                east_arrow = self.page_disposition[current_line][col_nb]
+                ctx.save()
+                ctx.translate(
+                    self._usable_area_width_pt \
+                     - commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
+                    self._usable_area_height_pt/2)
+                ctx.rotate(math.pi/2)
+                self._draw_arrow(ctx, cairo_surface,
+                               east_arrow + nb_previous_pages, 
max_digit_number)
+                ctx.restore()
+                break
+
+    def render(self, cairo_surface, dpi, osm_date):
+        ctx = cairo.Context(cairo_surface)
+
+        self._render_front_page(ctx, cairo_surface, dpi, osm_date)
+        self._render_blank_page(ctx, cairo_surface, dpi)
+
+        ctx.save()
+
+        # Prepare to draw the map at the right location
+        ctx.translate(
+                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
+                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
+
+        self._render_overview_page(ctx, cairo_surface, dpi)
+
+        for map_number, (canvas, grid) in enumerate(self.pages):
+
+            rendered_map = canvas.get_rendered_map()
+            LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator())
+            LOG.debug('Actual scale: 1/%f' % canvas.get_actual_scale())
+            mapnik.render(rendered_map, ctx)
+
+            # Place the vertical and horizontal square labels
+            ctx.save()
+            ctx.translate(commons.convert_pt_to_dots(self.grayed_margin_pt),
+                      commons.convert_pt_to_dots(self.grayed_margin_pt))
+            self._draw_labels(ctx, grid,
+                  commons.convert_pt_to_dots(self._usable_area_width_pt) \
+                        - 2 * 
commons.convert_pt_to_dots(self.grayed_margin_pt),
+                  commons.convert_pt_to_dots(self._usable_area_height_pt) \
+                        - 2 * 
commons.convert_pt_to_dots(self.grayed_margin_pt),
+                  commons.convert_pt_to_dots(self._grid_legend_margin_pt))
+
+            ctx.restore()
+
+            # Render the page number
+            draw_utils.render_page_number(ctx, map_number+4,
+                                          self._usable_area_width_pt,
+                                          self._usable_area_height_pt,
+                                          self.grayed_margin_pt,
+                                          transparent_background = True)
+            self._render_neighbour_arrows(ctx, cairo_surface, map_number,
+                                          len(unicode(len(self.pages)+4)))
+
+            cairo_surface.show_page()
+        ctx.restore()
+
+        mpsir = MultiPageStreetIndexRenderer(self.rc.i18n,
+                                             ctx, cairo_surface,
+                                             self.index_categories,
+                                             (Renderer.PRINT_SAFE_MARGIN_PT,
+                                              Renderer.PRINT_SAFE_MARGIN_PT,
+                                              self._usable_area_width_pt,
+                                              self._usable_area_height_pt),
+                                              map_number+5)
+
+        mpsir.render()
+
+        cairo_surface.flush()
+
+    # In multi-page mode, we only render pdf format
+    @staticmethod
+    def get_compatible_output_formats():
+        return [ "pdf" ]
+
+    # In multi-page mode, we only accept A4, A5 and US letter as paper
+    # sizes. The goal is to render booklets, not posters.
+    # The default paper size is A4 portrait
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box,
+                                   scale=Renderer.DEFAULT_SCALE,
+                                   index_position=None, hsplit=1, vsplit=1):
+        valid_sizes = []
+        acceptable_formats = [ 'A5', 'A4', 'US letter' ]
+        for sz in ocitysmap.layoutlib.PAPER_SIZES:
+            # Skip unsupported paper formats
+            if sz[0] not in acceptable_formats:
+                continue
+            valid_sizes.append((sz[0], sz[1], sz[2], True, True, sz[0] == 
'A4'))
+        return valid_sizes
+
+    @classmethod
+    def _draw_overview_labels(cls, ctx, map_canvas, overview_grid,
+                     area_width_dots, area_height_dots):
+        """
+        Draw the page numbers for the overview grid.
+
+        Args:
+           ctx (cairo.Context): The cairo context to use to draw.
+           overview_grid (OverViewGrid): the overview grid object
+           area_width_dots/area_height_dots (numbers): size of the
+              drawing area (cairo units).
+        """
+        ctx.save()
+        ctx.set_font_size(14)
+
+        bbox = map_canvas.get_actual_bounding_box()
+        bottom_right, bottom_left, top_left, top_right = bbox.to_mercator()
+        bottom, left = bottom_right.y, top_left.x
+        coord_delta_y = top_left.y - bottom_right.y
+        coord_delta_x = bottom_right.x - top_left.x
+        w, h = None, None
+        for idx, page_bb in enumerate(overview_grid._pages_bbox):
+            p_bottom_right, p_bottom_left, p_top_left, p_top_right = \
+                                                        page_bb.to_mercator()
+            center_x = p_top_left.x+(p_top_right.x-p_top_left.x)/2
+            center_y = p_bottom_left.y+(p_top_right.y-p_bottom_right.y)/2
+            y_percent = 100 - 100.0*(center_y - bottom)/coord_delta_y
+            y = int(area_height_dots*y_percent/100)
+
+            x_percent = 100.0*(center_x - left)/coord_delta_x
+            x = int(area_width_dots*x_percent/100)
+
+            if not w or not h:
+                w = area_width_dots*(p_bottom_right.x - p_bottom_left.x
+                                                         )/coord_delta_x
+                h = area_height_dots*(p_top_right.y - p_bottom_right.y
+                                                         )/coord_delta_y
+            draw_utils.draw_text_adjusted(ctx, unicode(idx+4), x, y, w, h,
+                 
max_char_number=len(unicode(len(overview_grid._pages_bbox)+3)),
+                 text_color=(0, 0, 0, 0.6))
+
+        ctx.restore()
diff --git a/ocitysmap/layoutlib/renderers.py b/ocitysmap/layoutlib/renderers.py
new file mode 100644
index 0000000..7c739cd
--- /dev/null
+++ b/ocitysmap/layoutlib/renderers.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+import single_page_renderers
+import multi_page_renderer
+
+# The renderers registry
+_RENDERERS = [
+    single_page_renderers.SinglePageRendererIndexBottom,
+    single_page_renderers.SinglePageRendererIndexOnSide,
+    single_page_renderers.SinglePageRendererNoIndex,
+    multi_page_renderer.MultiPageRenderer,
+    ]
+
+def get_renderer_class_by_name(name):
+    """Retrieves a renderer class, by name."""
+    for renderer in _RENDERERS:
+        if renderer.name == name:
+            return renderer
+    raise LookupError, 'The requested renderer %s was not found!' % name
+
+def get_renderers():
+    """Returns the list of available renderers' names."""
+    return _RENDERERS
diff --git a/ocitysmap/layoutlib/single_page_renderers.py 
b/ocitysmap/layoutlib/single_page_renderers.py
new file mode 100644
index 0000000..48e5a62
--- /dev/null
+++ b/ocitysmap/layoutlib/single_page_renderers.py
@@ -0,0 +1,690 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import cairo
+import datetime
+import locale
+import logging
+import mapnik
+assert mapnik.mapnik_version >= 200100, \
+    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
+    "for more details." % mapnik.mapnik_version_string()
+import math
+import pango
+import pangocairo
+
+import commons
+import ocitysmap
+from abstract_renderer import Renderer
+from ocitysmap.indexlib.renderer import StreetIndexRenderer
+from indexlib.indexer import StreetIndex
+from indexlib.commons import IndexDoesNotFitError, IndexEmptyError
+import draw_utils
+
+LOG = logging.getLogger('ocitysmap')
+
+
+class SinglePageRenderer(Renderer):
+    """
+    This Renderer creates a full-page map, with the overlayed features
+    like the grid, grid labels, scale and compass rose and can draw an
+    index.
+    """
+
+    name = 'generic_single_page'
+    description = 'A generic full-page layout with or without index.'
+
+    MAX_INDEX_OCCUPATION_RATIO = 1/3.
+
+    def __init__(self, db, rc, tmpdir, dpi, file_prefix,
+                 index_position = 'side'):
+        """
+        Create the renderer.
+
+        Args:
+           rc (RenderingConfiguration): rendering parameters.
+           tmpdir (os.path): Path to a temp dir that can hold temp files.
+           index_position (str): None or 'side' (index on side),
+              'bottom' (index at bottom).
+        """
+        Renderer.__init__(self, db, rc, tmpdir, dpi)
+
+        # Prepare the index
+        self.street_index = StreetIndex(db,
+                                        rc.polygon_wkt,
+                                        rc.i18n)
+        if not self.street_index.categories:
+            LOG.warning("Designated area leads to an empty index")
+            self.street_index = None
+
+        self._grid_legend_margin_pt = \
+            min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt,
+                Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt)
+        self._title_margin_pt = 0.05 * self.paper_height_pt
+        self._copyright_margin_pt = 0.02 * self.paper_height_pt
+
+        self._usable_area_width_pt = (self.paper_width_pt -
+                                      2 * Renderer.PRINT_SAFE_MARGIN_PT)
+        self._usable_area_height_pt = (self.paper_height_pt -
+                                       (2 * Renderer.PRINT_SAFE_MARGIN_PT +
+                                        self._title_margin_pt +
+                                        self._copyright_margin_pt))
+
+        # Prepare the Index (may raise a IndexDoesNotFitError)
+        if ( index_position and self.street_index
+             and self.street_index.categories ):
+            self._index_renderer, self._index_area \
+                = self._create_index_rendering(index_position == "side")
+        else:
+            self._index_renderer, self._index_area = None, None
+
+        # Prepare the layout of the whole page
+        if not self._index_area:
+            # No index displayed
+            self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
+                                 ( Renderer.PRINT_SAFE_MARGIN_PT
+                                   + self._title_margin_pt ),
+                                 self._usable_area_width_pt,
+                                 self._usable_area_height_pt )
+        elif index_position == 'side':
+            # Index present, displayed on the side
+            if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT:
+                # Index on the right -> map on the left
+                self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
+                                     ( Renderer.PRINT_SAFE_MARGIN_PT
+                                       + self._title_margin_pt ),
+                                     ( self._usable_area_width_pt
+                                       - self._index_area.w ),
+                                     self._usable_area_height_pt )
+            else:
+                # Index on the left -> map on the right
+                self._map_coords = ( self._index_area.x + self._index_area.w,
+                                     ( Renderer.PRINT_SAFE_MARGIN_PT
+                                       + self._title_margin_pt ),
+                                     ( self._usable_area_width_pt
+                                       - self._index_area.w ),
+                                     self._usable_area_height_pt )
+        elif index_position == 'bottom':
+            # Index present, displayed at the bottom -> map on top
+            self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
+                                 ( Renderer.PRINT_SAFE_MARGIN_PT
+                                   + self._title_margin_pt ),
+                                 self._usable_area_width_pt,
+                                 ( self._usable_area_height_pt
+                                   - self._index_area.h ) )
+        else:
+            raise AssertionError("Invalid index position %s"
+                                 % repr(index_position))
+
+        # Prepare the map
+        self._map_canvas = self._create_map_canvas(
+            float(self._map_coords[2]),  # W
+            float(self._map_coords[3]),  # H
+            dpi )
+
+        # Prepare the grid
+        self.grid = self._create_grid(self._map_canvas)
+
+        # Update the street_index to reflect the grid's actual position
+        if self.grid and self.street_index:
+            self.street_index.apply_grid(self.grid)
+
+        # Dump the CSV street index
+        if self.street_index:
+            self.street_index.write_to_csv(rc.title, '%s.csv' % file_prefix)
+
+        # Commit the internal rendering stack of the map
+        self._map_canvas.render()
+
+
+    def _create_index_rendering(self, on_the_side):
+        """
+        Prepare to render the Street index.
+
+        Args:
+           on_the_side (bool): True=index on the side, False=at bottom.
+
+        Return a couple (StreetIndexRenderer, StreetIndexRenderingArea).
+        """
+        # Now we determine the actual occupation of the index
+        index_renderer = StreetIndexRenderer(self.rc.i18n,
+                                             self.street_index.categories)
+
+        # We use a fake vector device to determine the actual
+        # rendering characteristics
+        fake_surface = cairo.PDFSurface(None,
+                                        self.paper_width_pt,
+                                        self.paper_height_pt)
+
+        if on_the_side:
+            index_max_width_pt \
+                = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_width_pt
+
+            if not self.rc.i18n.isrtl():
+                # non-RTL: Index is on the right
+                index_area = index_renderer.precompute_occupation_area(
+                    fake_surface,
+                    ( self.paper_width_pt - Renderer.PRINT_SAFE_MARGIN_PT
+                      - index_max_width_pt ),
+                    ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ),
+                    index_max_width_pt,
+                    self._usable_area_height_pt,
+                    'width', 'right')
+            else:
+                # RTL: Index is on the left
+                index_area = index_renderer.precompute_occupation_area(
+                    fake_surface,
+                    Renderer.PRINT_SAFE_MARGIN_PT,
+                    ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ),
+                    index_max_width_pt,
+                    self._usable_area_height_pt,
+                    'width', 'left')
+        else:
+            # Index at the bottom of the page
+            index_max_height_pt \
+                = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_height_pt
+
+            index_area = index_renderer.precompute_occupation_area(
+                fake_surface,
+                Renderer.PRINT_SAFE_MARGIN_PT,
+                ( self.paper_height_pt
+                  - Renderer.PRINT_SAFE_MARGIN_PT
+                  - self._copyright_margin_pt
+                  - index_max_height_pt ),
+                self._usable_area_width_pt,
+                index_max_height_pt,
+                'height', 'bottom')
+
+        return index_renderer, index_area
+
+
+    def _draw_title(self, ctx, w_dots, h_dots, font_face):
+        """
+        Draw the title at the current position inside a
+        w_dots*h_dots rectangle.
+
+        Args:
+           ctx (cairo.Context): The Cairo context to use to draw.
+           w_dots,h_dots (number): Rectangle dimension (ciaro units)
+           font_face (str): Pango font specification.
+        """
+
+        # Title background
+        ctx.save()
+        ctx.set_source_rgb(0.8, 0.9, 0.96)
+        ctx.rectangle(0, 0, w_dots, h_dots)
+        ctx.fill()
+        ctx.restore()
+
+        # Retrieve and paint the OSM logo
+        ctx.save()
+        grp, logo_width = self._get_osm_logo(ctx, 0.8*h_dots)
+        if grp:
+            ctx.translate(w_dots - logo_width - 0.1*h_dots, 0.1*h_dots)
+            ctx.set_source(grp)
+            ctx.paint_with_alpha(0.5)
+        else:
+            LOG.warning("OSM Logo not available.")
+            logo_width = 0
+        ctx.restore()
+
+        # Prepare the title
+        pc = pangocairo.CairoContext(ctx)
+        layout = pc.create_layout()
+        layout.set_width(int((w_dots - 0.1*w_dots - logo_width) * pango.SCALE))
+        if not self.rc.i18n.isrtl(): layout.set_alignment(pango.ALIGN_LEFT)
+        else:                        layout.set_alignment(pango.ALIGN_RIGHT)
+        fd = pango.FontDescription(font_face)
+        fd.set_size(pango.SCALE)
+        layout.set_font_description(fd)
+        layout.set_text(self.rc.title)
+        draw_utils.adjust_font_size(layout, fd, layout.get_width(), 0.8*h_dots)
+
+        # Draw the title
+        ctx.save()
+        ctx.rectangle(0, 0, w_dots, h_dots)
+        ctx.stroke()
+        ctx.translate(0.1*h_dots,
+                      (h_dots -
+                       (layout.get_size()[1] / pango.SCALE)) / 2.0)
+        pc.show_layout(layout)
+        ctx.restore()
+
+
+    def _draw_copyright_notice(self, ctx, w_dots, h_dots, notice=None,
+                               osm_date=None):
+        """
+        Draw a copyright notice at current location and within the
+        given w_dots*h_dots rectangle.
+
+        Args:
+           ctx (cairo.Context): The Cairo context to use to draw.
+           w_dots,h_dots (number): Rectangle dimension (ciaro units).
+           font_face (str): Pango font specification.
+           notice (str): Optional notice to replace the default.
+        """
+
+        today = datetime.date.today()
+        notice = notice or \
+            _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers. '
+              u'Map data © %(year)d OpenStreetMap.org '
+              u'and contributors (cc-by-sa).\n'
+              u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s. '
+              u'The map may be incomplete or inaccurate. '
+              u'You can contribute to improve this map. '
+              u'See http://wiki.openstreetmap.org')
+
+        # We need the correct locale to be set for strftime().
+        prev_locale = locale.getlocale(locale.LC_TIME)
+        locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code())
+        try:
+            if osm_date is None:
+                osm_date_str = _(u'unknown')
+            else:
+                osm_date_str = osm_date.strftime("%d %B %Y %H:%M")
+
+            notice = notice % {'year': today.year,
+                               'date': today.strftime("%d %B %Y"),
+                               'osmdate': osm_date_str}
+        finally:
+            locale.setlocale(locale.LC_TIME, prev_locale)
+
+        ctx.save()
+        pc = pangocairo.CairoContext(ctx)
+        fd = pango.FontDescription('DejaVu')
+        fd.set_size(pango.SCALE)
+        layout = pc.create_layout()
+        layout.set_font_description(fd)
+        layout.set_text(notice)
+        draw_utils.adjust_font_size(layout, fd, w_dots, h_dots)
+        pc.show_layout(layout)
+        ctx.restore()
+
+
+    def render(self, cairo_surface, dpi, osm_date):
+        """Renders the map, the index and all other visual map features on the
+        given Cairo surface.
+
+        Args:
+            cairo_surface (Cairo.Surface): the destination Cairo device.
+            dpi (int): dots per inch of the device.
+        """
+        LOG.info('SinglePageRenderer rendering on %dx%dmm paper at %d dpi.' %
+                 (self.rc.paper_width_mm, self.rc.paper_height_mm, dpi))
+
+        # First determine some useful drawing parameters
+        safe_margin_dots \
+            = commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT, dpi)
+        usable_area_width_dots \
+            = commons.convert_pt_to_dots(self._usable_area_width_pt, dpi)
+        usable_area_height_dots \
+            = commons.convert_pt_to_dots(self._usable_area_height_pt, dpi)
+
+        title_margin_dots \
+            = commons.convert_pt_to_dots(self._title_margin_pt, dpi)
+
+        copyright_margin_dots \
+            = commons.convert_pt_to_dots(self._copyright_margin_pt, dpi)
+
+        map_coords_dots = map(lambda l: commons.convert_pt_to_dots(l, dpi),
+                              self._map_coords)
+
+        ctx = cairo.Context(cairo_surface)
+
+        # Set a white background
+        ctx.save()
+        ctx.set_source_rgb(1, 1, 1)
+        ctx.rectangle(0, 0, commons.convert_pt_to_dots(self.paper_width_pt, 
dpi),
+                      commons.convert_pt_to_dots(self.paper_height_pt, dpi))
+        ctx.fill()
+        ctx.restore()
+
+        ##
+        ## Draw the index, when applicable
+        ##
+        if self._index_renderer and self._index_area:
+            ctx.save()
+
+            # NEVER use ctx.scale() here because otherwise pango will
+            # choose different dont metrics which may be incompatible
+            # with what has been computed by __init__(), which may
+            # require more columns than expected !  Instead, we have
+            # to trick pangocairo into believing it is rendering to a
+            # device with the same default resolution, but with a
+            # cairo resolution matching the 'dpi' specified
+            # resolution. See
+            # index::render::StreetIndexRenederer::render() and
+            # comments within.
+
+            self._index_renderer.render(ctx, self._index_area, dpi)
+
+            ctx.restore()
+
+            # Also draw a rectangle
+            ctx.save()
+            ctx.rectangle(commons.convert_pt_to_dots(self._index_area.x, dpi),
+                          commons.convert_pt_to_dots(self._index_area.y, dpi),
+                          commons.convert_pt_to_dots(self._index_area.w, dpi),
+                          commons.convert_pt_to_dots(self._index_area.h, dpi))
+            ctx.stroke()
+            ctx.restore()
+
+
+        ##
+        ## Draw the map, scaled to fit the designated area
+        ##
+        ctx.save()
+
+        # Prepare to draw the map at the right location
+        ctx.translate(map_coords_dots[0], map_coords_dots[1])
+
+        # Draw the rescaled Map
+        ctx.save()
+        rendered_map = self._map_canvas.get_rendered_map()
+        LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator())
+        LOG.debug('Actual scale: 1/%f' % self._map_canvas.get_actual_scale())
+        mapnik.render(rendered_map, ctx)
+        ctx.restore()
+
+        # Draw a rectangle around the map
+        ctx.rectangle(0, 0, map_coords_dots[2], map_coords_dots[3])
+        ctx.stroke()
+
+        # Place the vertical and horizontal square labels
+        self._draw_labels(ctx, self.grid,
+                          map_coords_dots[2],
+                          map_coords_dots[3],
+                          
commons.convert_pt_to_dots(self._grid_legend_margin_pt,
+                                                   dpi))
+        ctx.restore()
+
+        ##
+        ## Draw the title
+        ##
+        ctx.save()
+        ctx.translate(safe_margin_dots, safe_margin_dots)
+        self._draw_title(ctx, usable_area_width_dots,
+                         title_margin_dots, 'Georgia Bold')
+        ctx.restore()
+
+        ##
+        ## Draw the copyright notice
+        ##
+        ctx.save()
+
+        # Move to the right position
+        ctx.translate(safe_margin_dots,
+                      ( safe_margin_dots + title_margin_dots
+                        + usable_area_height_dots
+                        + copyright_margin_dots/4. ) )
+
+        # Draw the copyright notice
+        self._draw_copyright_notice(ctx, usable_area_width_dots,
+                                    copyright_margin_dots,
+                                    osm_date=osm_date)
+        ctx.restore()
+
+        # TODO: map scale
+        # TODO: compass rose
+
+        cairo_surface.flush()
+
+    @staticmethod
+    def _generic_get_compatible_paper_sizes(bounding_box,
+                                            scale=Renderer.DEFAULT_SCALE, 
index_position = None):
+        """Returns a list of the compatible paper sizes for the given bounding
+        box. The list is sorted, smaller papers first, and a "custom" paper
+        matching the dimensions of the bounding box is added at the end.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map geographic bounding box.
+            scale (int): minimum mapnik scale of the map.
+           index_position (str): None or 'side' (index on side),
+              'bottom' (index at bottom).
+
+        Returns a list of tuples (paper name, width in mm, height in
+        mm, portrait_ok, landscape_ok, is_default). Paper sizes are
+        represented in portrait mode.
+        """
+
+        # the mapnik scale depends on the latitude
+        lat = bounding_box.get_top_left()[0]
+        scale *= math.cos(math.radians(lat))
+
+        # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi
+        scale *= float(72) / 90
+
+        geo_height_m, geo_width_m = bounding_box.spheric_sizes()
+        paper_width_mm = geo_width_m * 1000 / scale
+        paper_height_mm = geo_height_m * 1000 / scale
+
+        LOG.debug('Map represents %dx%dm, needs at least %.1fx%.1fcm '
+                  'on paper.' % (geo_width_m, geo_height_m,
+                                 paper_width_mm/10., paper_height_mm/10.))
+
+        # Take index into account, when applicable
+        if index_position == 'side':
+            paper_width_mm /= (1. -
+                               SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO)
+        elif index_position == 'bottom':
+            paper_height_mm /= (1. -
+                                SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO)
+
+        # Take margins into account
+        paper_width_mm += 2 * 
commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT)
+        paper_height_mm += 2 * 
commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT)
+
+        # Take grid legend, title and copyright into account
+        paper_width_mm /= 1 - Renderer.GRID_LEGEND_MARGIN_RATIO
+        paper_height_mm /= 1 - (Renderer.GRID_LEGEND_MARGIN_RATIO + 0.05 + 
0.02)
+
+        # Transform the values into integers
+        paper_width_mm  = int(math.ceil(paper_width_mm))
+        paper_height_mm = int(math.ceil(paper_height_mm))
+
+        LOG.debug('Best fit is %.1fx%.1fcm.' % (paper_width_mm/10., 
paper_height_mm/10.))
+
+        # Test both portrait and landscape orientations when checking for paper
+        # sizes.
+        valid_sizes = []
+        for name, w, h in ocitysmap.layoutlib.PAPER_SIZES:
+            portrait_ok  = paper_width_mm <= w and paper_height_mm <= h
+            landscape_ok = paper_width_mm <= h and paper_height_mm <= w
+
+            if portrait_ok or landscape_ok:
+                valid_sizes.append([name, w, h, portrait_ok, landscape_ok, 
False])
+
+        # Add a 'Custom' paper format to the list that perfectly matches the
+        # bounding box.
+        valid_sizes.append(['Best fit',
+                            min(paper_width_mm, paper_height_mm),
+                            max(paper_width_mm, paper_height_mm),
+                            paper_width_mm < paper_height_mm,
+                            paper_width_mm > paper_height_mm,
+                            False])
+
+        # select the first one as default
+        valid_sizes[0][5] = True
+
+        return valid_sizes
+
+
+class SinglePageRendererNoIndex(SinglePageRenderer):
+
+    name = 'plain'
+    description = 'Full-page layout without index.'
+
+    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
+        """
+        Create the renderer.
+
+        Args:
+           rc (RenderingConfiguration): rendering parameters.
+           tmpdir (os.path): Path to a temp dir that can hold temp files.
+        """
+        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
None)
+
+
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box,
+                                   scale=Renderer.DEFAULT_SCALE):
+        """Returns a list of the compatible paper sizes for the given bounding
+        box. The list is sorted, smaller papers first, and a "custom" paper
+        matching the dimensions of the bounding box is added at the end.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map geographic bounding box.
+            scale (int): minimum mapnik scale of the map.
+
+        Returns a list of tuples (paper name, width in mm, height in
+        mm, portrait_ok, landscape_ok). Paper sizes are represented in
+        portrait mode.
+        """
+        return SinglePageRenderer._generic_get_compatible_paper_sizes(
+            bounding_box, scale, None)
+
+
+class SinglePageRendererIndexOnSide(SinglePageRenderer):
+
+    name = 'single_page_index_side'
+    description = 'Full-page layout with the index on the side.'
+
+    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
+        """
+        Create the renderer.
+
+        Args:
+           rc (RenderingConfiguration): rendering parameters.
+           tmpdir (os.path): Path to a temp dir that can hold temp files.
+        """
+        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
'side')
+
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box,
+                                   scale=Renderer.DEFAULT_SCALE):
+        """Returns a list of the compatible paper sizes for the given bounding
+        box. The list is sorted, smaller papers first, and a "custom" paper
+        matching the dimensions of the bounding box is added at the end.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map geographic bounding box.
+            scale (int): minimum mapnik scale of the map.
+
+        Returns a list of tuples (paper name, width in mm, height in
+        mm, portrait_ok, landscape_ok). Paper sizes are represented in
+        portrait mode.
+        """
+        return SinglePageRenderer._generic_get_compatible_paper_sizes(
+            bounding_box, scale, 'side')
+
+
+class SinglePageRendererIndexBottom(SinglePageRenderer):
+
+    name = 'single_page_index_bottom'
+    description = 'Full-page layout with the index at the bottom.'
+
+    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
+        """
+        Create the renderer.
+
+        Args:
+           rc (RenderingConfiguration): rendering parameters.
+           tmpdir (os.path): Path to a temp dir that can hold temp files.
+        """
+        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
'bottom')
+
+    @staticmethod
+    def get_compatible_paper_sizes(bounding_box,
+                                   scale=Renderer.DEFAULT_SCALE):
+        """Returns a list of the compatible paper sizes for the given bounding
+        box. The list is sorted, smaller papers first, and a "custom" paper
+        matching the dimensions of the bounding box is added at the end.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map geographic bounding box.
+            scale (int): minimum mapnik scale of the map.
+
+        Returns a list of tuples (paper name, width in mm, height in
+        mm, portrait_ok, landscape_ok). Paper sizes are represented in
+        portrait mode.
+        """
+        return SinglePageRenderer._generic_get_compatible_paper_sizes(
+            bounding_box, scale, 'bottom')
+
+
+if __name__ == '__main__':
+    import renderers
+    import coords
+    from ocitysmap import i18n
+
+    # Hack to fake gettext
+    try:
+        _(u"Test gettext")
+    except NameError:
+        __builtins__.__dict__["_"] = lambda x: x
+
+    logging.basicConfig(level=logging.DEBUG)
+
+    bbox = coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699)
+    zoom = 16
+
+    renderer_cls = renderers.get_renderer_class_by_name('plain')
+    papers = renderer_cls.get_compatible_paper_sizes(bbox, zoom)
+
+    print 'Compatible paper sizes:'
+    for p in papers:
+        print '  * %s (%.1fx%.1fcm)' % (p[0], p[1]/10.0, p[2]/10.0)
+    print 'Using first available:', papers[0]
+
+    class StylesheetMock:
+        def __init__(self):
+            # self.path = '/home/sam/src/python/maposmatic/mapnik-osm/osm.xml'
+            self.path = 
'/mnt/data1/common/home/d2/Downloads/svn/mapnik-osm/osm.xml'
+            self.grid_line_color = 'black'
+            self.grid_line_alpha = 0.9
+            self.grid_line_width = 2
+            self.shade_color = 'black'
+            self.shade_alpha = 0.7
+
+    class RenderingConfigurationMock:
+        def __init__(self):
+            self.stylesheet = StylesheetMock()
+            self.bounding_box = bbox
+            self.paper_width_mm = papers[0][1]
+            self.paper_height_mm = papers[0][2]
+            self.i18n  = i18n.i18n()
+            self.title = 'Au Kremlin-Bycêtre'
+            self.polygon_wkt = bbox.as_wkt()
+
+    config = RenderingConfigurationMock()
+
+    plain = renderer_cls(config, '/tmp', None)
+    surface = cairo.PDFSurface('/tmp/plain.pdf',
+                   commons.convert_mm_to_pt(config.paper_width_mm),
+                   commons.convert_mm_to_pt(config.paper_height_mm))
+
+    plain.render(surface, commons.PT_PER_INCH)
+    surface.finish()
+
+    print "Generated /tmp/plain.pdf"
diff --git a/ocitysmap/maplib/__init__.py b/ocitysmap/maplib/__init__.py
new file mode 100644
index 0000000..dad0e7e
--- /dev/null
+++ b/ocitysmap/maplib/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
diff --git a/ocitysmap/maplib/grid.py b/ocitysmap/maplib/grid.py
new file mode 100644
index 0000000..9c1ff68
--- /dev/null
+++ b/ocitysmap/maplib/grid.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import math
+
+import shapes
+
+l = logging.getLogger('ocitysmap')
+
+class Grid:
+    """
+    The Grid class defines the grid overlayed on a rendered map. It controls
+    the grid size, number and size of squares, etc.
+    """
+
+    # Approximative paper size of the grid squares (+/- 33%).
+    GRID_APPROX_PAPER_SIZE_MM = 40
+
+    def __init__(self, bounding_box, scale, rtl=False):
+        """Creates a new grid for the given bounding box.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map bounding box.
+            rtl (boolean): whether the map is rendered in right-to-left mode or
+                not. Defaults to False.
+        """
+
+        self._bbox = bounding_box
+        self.rtl   = rtl
+        self._height_m, self._width_m = bounding_box.spheric_sizes()
+
+        l.info('Laying out grid on %.1fx%.1fm area...' %
+               (self._width_m, self._height_m))
+
+        # compute the terrain grid size corresponding to the targeted paper 
size
+        size = float(self.GRID_APPROX_PAPER_SIZE_MM) * scale / 1000
+        # compute the scientific notation of this size :
+        # size = significant * 10 ^ exponent with 1 <= significand < 10
+        exponent = math.log10(size)
+        significand = float(size) / 10 ** int(exponent)
+        # "round" this size to be 1, 2, 2.5 or 5 multiplied by a power of 10
+        if significand < 1.5:
+            significand = 1
+        elif significand < 2.25:
+            significand = 2
+        elif significand < 3.75:
+            significand = 2.5
+        elif significand < 7.5:
+            significand = 5
+        else:
+            significand = 10
+        size = significand * 10 ** int(exponent)
+        # use it
+        self.grid_size_m = size
+        self.horiz_count = self._width_m / size
+        self.vert_count = self._height_m / size
+
+        self._horiz_angle_span = abs(self._bbox.get_top_left()[1] -
+                                     self._bbox.get_bottom_right()[1])
+        self._vert_angle_span  = abs(self._bbox.get_top_left()[0] -
+                                     self._bbox.get_bottom_right()[0])
+
+        self._horiz_unit_angle = (self._horiz_angle_span / self.horiz_count)
+        self._vert_unit_angle  = (self._vert_angle_span / self.vert_count)
+
+        self._horizontal_lines = [ ( self._bbox.get_top_left()[0] -
+                                    (x+1) * self._vert_unit_angle)
+                                  for x in 
xrange(int(math.floor(self.vert_count)))]
+        self._vertical_lines   = [ (self._bbox.get_top_left()[1] +
+                                    (x+1) * self._horiz_unit_angle)
+                                   for x in 
xrange(int(math.floor(self.horiz_count)))]
+
+        self.horizontal_labels = map(self._gen_horizontal_square_label,
+                                      xrange(int(math.ceil(self.horiz_count))))
+        self.vertical_labels = map(self._gen_vertical_square_label,
+                                   xrange(int(math.ceil(self.vert_count))))
+
+        l.info('Using %sx%sm grid (%sx%s squares).' %
+               (self.grid_size_m, self.grid_size_m,
+                self.horiz_count, self.vert_count))
+
+    def generate_shape_file(self, filename):
+        """Generates the grid shapefile with all the horizontal and
+        vertical lines added.
+
+        Args:
+            filename (string): path to the temporary shape file that will be
+                generated.
+        Returns the ShapeFile object.
+        """
+
+        # Use a slightly larger bounding box for the shape file to accomodate
+        # for the small imprecisions of re-projecting.
+        g = shapes.LineShapeFile(self._bbox.create_expanded(0.001, 0.001),
+                                 filename, 'grid')
+        map(g.add_vert_line, self._vertical_lines)
+        map(g.add_horiz_line, self._horizontal_lines)
+        return g
+
+    def _gen_horizontal_square_label(self, x):
+        """Generates a human-readable label for the given horizontal square
+        number. For example:
+             1 -> A
+             2 -> B
+            26 -> Z
+            27 -> AA
+            28 -> AB
+            ...
+        """
+        if self.rtl:
+            x = len(self._vertical_lines) - x
+
+        label = ''
+        while x != -1:
+            label = chr(ord('A') + x % 26) + label
+            x = x/26 - 1
+        return label
+
+    def _gen_vertical_square_label(self, x):
+        """Generate a human-readable label for the given vertical square
+        number. Since we put numbers verticaly, this is simply x+1."""
+        return str(x + 1)
+
+    def get_location_str(self, lattitude, longitude):
+        """
+        Translate the given lattitude/longitude (EPSG:4326) into a
+        string of the form "CA42"
+        """
+        hdelta = min(abs(longitude - self._bbox.get_top_left()[1]),
+                     self._horiz_angle_span)
+        hlabel = self.horizontal_labels[int(hdelta / self._horiz_unit_angle)]
+
+        vdelta = min(abs(lattitude - self._bbox.get_top_left()[0]),
+                     self._vert_angle_span)
+        vlabel = self.vertical_labels[int(vdelta / self._vert_unit_angle)]
+
+        return "%s%s" % (hlabel, vlabel)
+
+
+if __name__ == "__main__":
+    import ocitysmap
+
+    logging.basicConfig(level=logging.DEBUG)
+    grid = Grid(ocitysmap.coords.BoundingBox(44.4883, -1.0901, 44.4778, 
-1.0637))
+    shape = grid.generate_shape_file('/tmp/mygrid.shp')
diff --git a/ocitysmap/maplib/map_canvas.py b/ocitysmap/maplib/map_canvas.py
new file mode 100644
index 0000000..1647fe4
--- /dev/null
+++ b/ocitysmap/maplib/map_canvas.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+
+# Importing mapnik2 raises a DeprecationWarning as of mapnik
+# commit 14700dba. As mapnik 2.1 (or git version with support for
+# placement-type="simple") is required for OCitySMap (see INSTALL),
+# instead of importing mapnik2, we import mapnik and assert it isn't
+# an old version.
+import mapnik
+assert mapnik.mapnik_version >= 200100, \
+    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
+    "for more details." % mapnik.mapnik_version_string()
+
+import math
+import os
+
+import ocitysmap
+from layoutlib.commons import convert_pt_to_dots
+import shapes
+
+l = logging.getLogger('ocitysmap')
+
+_MAPNIK_PROJECTION = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 " \
+                     "+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m   " \
+                     "address@hidden +no_defs +over"
+
+class MapCanvas:
+    """
+    The MapCanvas renders a geographic bounding box into a Cairo surface of a
+    given width and height (in pixels). Shape files can be overlayed on the
+    map; the order they are added to the map being important with regard to
+    their respective alpha levels.
+    """
+
+    def __init__(self, stylesheet, bounding_box, _width, _height, dpi,
+                 extend_bbox_to_ratio=True):
+        """Initialize the map canvas for rendering.
+
+        Args:
+            stylesheet (Stylesheet): map stylesheet.
+            bounding_box (coords.BoundingBox): geographic bounding box.
+            graphical_ratio (float): ratio of the map area (width/height).
+            extend_bbox_to_ratio (boolean): allow MapCanvas to extend
+            the bounding box to make it match the ratio of the
+            provided rendering area. Needed by SinglePageRenderer.
+        """
+
+        self._proj = mapnik.Projection(_MAPNIK_PROJECTION)
+
+        # This is where the magic of the map canvas happens. Given an original
+        # bounding box and a graphical ratio for the output, the bounding box
+        # is adjusted (extended) to fill the destination zone. See
+        # _fix_bbox_ratio for more details on how this is done.
+        orig_envelope = self._project_envelope(bounding_box)
+        graphical_ratio = _width / _height
+
+        if extend_bbox_to_ratio:
+            off_x, off_y, width, height = self._fix_bbox_ratio(
+                orig_envelope.minx, orig_envelope.miny,
+                orig_envelope.width(), orig_envelope.height(),
+                graphical_ratio)
+
+            envelope = mapnik.Box2d(off_x, off_y, off_x+width, off_y+height)
+            self._geo_bbox = self._inverse_envelope(envelope)
+            l.debug('Corrected bounding box from %s to %s, ratio: %.2f.' %
+                    (bounding_box, self._geo_bbox, graphical_ratio))
+        else:
+            envelope = orig_envelope
+            self._geo_bbox = bounding_box
+
+        g_width  = int(convert_pt_to_dots(_width, dpi))
+        g_height = int(convert_pt_to_dots(_height, dpi))
+
+        # Create the Mapnik map with the corrected width and height and zoom to
+        # the corrected bounding box ('envelope' in the Mapnik jargon)
+        self._map = mapnik.Map(g_width, g_height, _MAPNIK_PROJECTION)
+        mapnik.load_map(self._map, stylesheet.path)
+        self._map.zoom_to_box(envelope)
+
+        # Added shapes to render
+        self._shapes = []
+
+        l.info('MapCanvas rendering map on %dx%dpx.' % (g_width, g_height))
+
+    def _fix_bbox_ratio(self, off_x, off_y, width, height, dest_ratio):
+        """Adjusts the area expressed by its origin's offset and its size to
+        the given destination ratio by tweaking one of the two dimensions
+        depending on the current ratio and the destination ratio."""
+        cur_ratio = float(width)/height
+
+        if cur_ratio < dest_ratio:
+            w = width
+            width *= float(dest_ratio)/cur_ratio
+            off_x -= (width - w)/2.0
+        else:
+            h = height
+            height *= float(cur_ratio)/dest_ratio
+            off_y -= (height - h)/2.0
+
+        return map(int, (off_x, off_y, width, height))
+
+    def add_shape_file(self, shape_file, str_color='grey', alpha=0.5,
+                       line_width=1.0):
+        """
+        Args:
+            shape_file (shapes.ShapeFile): path to the shape file to overlay on
+                this map canvas.
+            str_color (string): litteral name of the layer's color, needs to be
+                understood by mapnik.Color.
+            alpha (float): transparency factor in the range 0 (invisible) -> 1
+                (opaque).
+            line_width (float): line width for the features that will be drawn.
+        """
+        col = mapnik.Color(str_color)
+        col.a = int(255 * alpha)
+        self._shapes.append({'shape_file': shape_file,
+                             'color': col,
+                             'line_width': line_width})
+        l.debug('Added shape file %s to map canvas as layer %s.' %
+                (shape_file.get_filepath(), shape_file.get_layer_name()))
+
+    def render(self):
+        """Render the map in memory with all the added shapes. The Mapnik Map
+        object can be accessed with self.get_rendered_map()."""
+
+        # Add all shapes to the map
+        for shape in self._shapes:
+            self._render_shape_file(**shape)
+
+    def get_rendered_map(self):
+        return self._map
+
+    def get_actual_bounding_box(self):
+        """Returns the actual geographic bounding box that will be rendered by
+        Mapnik."""
+        return self._geo_bbox
+
+    def get_actual_scale(self):
+        # get the scale denominator computed by mapnik
+        scale = self._map.scale_denominator()
+        # the actual scale depends on the latitude
+        lat = self._geo_bbox.get_top_left()[0]
+        scale *= math.cos(math.radians(lat))
+        # by convention, the scale denominator uses 90 ppi whereas cairo uses 
72 ppi
+        scale *= float(72) / 90
+        return scale
+
+    def _render_shape_file(self, shape_file, color, line_width):
+        shape_file.flush()
+
+        shpid = os.path.basename(shape_file.get_filepath())
+        s,r = mapnik.Style(), mapnik.Rule()
+        r.symbols.append(mapnik.PolygonSymbolizer(color))
+        r.symbols.append(mapnik.LineSymbolizer(color, line_width))
+        s.rules.append(r)
+
+        self._map.append_style('style_%s' % shpid, s)
+        layer = mapnik.Layer(shpid)
+        layer.datasource = mapnik.Shapefile(file=shape_file.get_filepath())
+        layer.styles.append('style_%s' % shpid)
+
+        self._map.layers.append(layer)
+
+    def _project_envelope(self, bbox):
+        """Project the given bounding box into the rendering projection."""
+        envelope = mapnik.Box2d(bbox.get_top_left()[1],
+                                bbox.get_top_left()[0],
+                                bbox.get_bottom_right()[1],
+                                bbox.get_bottom_right()[0])
+        c0 = self._proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return mapnik.Box2d(c0.x, c0.y, c1.x, c1.y)
+
+    def _inverse_envelope(self, envelope):
+        """Inverse the given cartesian envelope (in 900913) back to a 4002
+        bounding box."""
+        c0 = self._proj.inverse(mapnik.Coord(envelope.minx, envelope.miny))
+        c1 = self._proj.inverse(mapnik.Coord(envelope.maxx, envelope.maxy))
+        return ocitysmap.coords.BoundingBox(c0.y, c0.x, c1.y, c1.x)
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.DEBUG)
+
+    class StylesheetMock:
+        def __init__(self):
+            self.path = '/home/sam/src/python/maposmatic/mapnik-osm/osm.xml'
+
+    bbox = ocitysmap.coords.BoundingBox(48.7148, 2.0155, 48.6950, 2.0670)
+    canvas = MapCanvas(StylesheetMock(), bbox, 297.0/210)
+    new_bbox = canvas.get_actual_bounding_box()
+
+    canvas.add_shape_file(
+        shapes.LineShapeFile(new_bbox, '/tmp/mygrid.shp', 'grid')
+            .add_vert_line(2.04)
+            .add_horiz_line(48.7),
+        'red', 0.3, 10.0)
+
+    canvas.add_shape_file(
+        shapes.PolyShapeFile(new_bbox, '/tmp/mypoly.shp', 'shade')
+            .add_shade_from_wkt('POLYGON((2.04537559754772 
48.702794853359,2.0456929723376 48.7033682610593,2.0457757970068 
48.7037022715908,2.04577876144723 48.7043963708738,2.04589724923321 
48.7043963708738,2.04589428479277 48.704519562418,2.04746445007788 
48.7044706533954,2.04723043894637 48.7024665875529,2.04674876229103 
48.7024238422904,2.04615641319268 48.702500973452,2.04537559754772 
48.702794853359))'),
+        'blue', 0.3)
+
+    canvas.render()
+    mapnik.render_to_file(canvas.get_rendered_map(), '/tmp/mymap.png', 'png')
+
+    print "Generated /tmp/mymap.png"
diff --git a/ocitysmap/maplib/overview_grid.py 
b/ocitysmap/maplib/overview_grid.py
new file mode 100644
index 0000000..eb1bcea
--- /dev/null
+++ b/ocitysmap/maplib/overview_grid.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+
+# Copyright (C) 2012  Étienne Loks
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import logging
+import math
+
+import shapes
+
+l = logging.getLogger('ocitysmap')
+
+class OverviewGrid:
+    """
+    The OverviewGrid class draw the grid overlayed on the overview map of a
+    multi-page render.
+    """
+
+    def __init__(self, bounding_box, pages_bounding_boxes, rtl=False):
+        """Creates a new grid for the given bounding boxes.
+
+        Args:
+            bounding_box (coords.BoundingBox): the map bounding box.
+            bounding_box (list of coords.BoundingBox): bounding boxes of the
+                pages.
+            rtl (boolean): whether the map is rendered in right-to-left mode or
+                not. Defaults to False.
+        """
+
+        self._bbox = bounding_box
+        self._pages_bbox = pages_bounding_boxes
+        self.rtl   = rtl
+        self._height_m, self._width_m = bounding_box.spheric_sizes()
+
+        l.info('Laying out of overview grid on %.1fx%.1fm area...' %
+               (self._width_m, self._height_m))
+
+    def generate_shape_file(self, filename):
+        """Generates the grid shapefile with all the horizontal and
+        vertical lines added.
+
+        Args:
+            filename (string): path to the temporary shape file that will be
+                generated.
+        Returns the ShapeFile object.
+        """
+
+        # Use a slightly larger bounding box for the shape file to accomodate
+        # for the small imprecisions of re-projecting.
+        g = shapes.BoxShapeFile(self._bbox.create_expanded(0.001, 0.001),
+                                 filename, 'grid')
+        map(g.add_box, self._pages_bbox)
+        return g
+
+
+
+if __name__ == "__main__":
+    logging.basicConfig(level=logging.DEBUG)
+    pass
diff --git a/ocitysmap/maplib/shapes.py b/ocitysmap/maplib/shapes.py
new file mode 100644
index 0000000..fe915fd
--- /dev/null
+++ b/ocitysmap/maplib/shapes.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2010  David Decotigny
+# Copyright (C) 2010  Frédéric Lehobey
+# Copyright (C) 2010  Pierre Mauduit
+# Copyright (C) 2010  David Mentré
+# Copyright (C) 2010  Maxime Petazzoni
+# Copyright (C) 2010  Thomas Petazzoni
+# Copyright (C) 2010  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import locale
+import logging
+import os
+
+# The ogr module is now known as osgeo.ogr in recent versions of the
+# module, but we want to keep compatibility with older versions
+try:
+    from osgeo import ogr
+except ImportError:
+    import ogr
+
+l = logging.getLogger('ocitysmap')
+
+class _ShapeFile:
+    """
+    This class represents a shapefile (.shp) that can be added to a Mapnik map
+    as a layer. It provides a few methods to add some geometry 'features' to
+    the shape file.
+
+    This is a private base class and is not meant to be used directly from the
+    outside.
+    """
+
+    def __init__(self, bounding_box, out_filename, layer_name):
+        """
+        Args:
+            bounding_box (BoundingBox): bounding box of the map area.
+            out_filename (string): path to the output shape file to generate.
+            layer_name (string): layer name for the shape file.
+        """
+
+        self._bbox = bounding_box
+        self._filepath = out_filename
+        self._layer_name = layer_name
+
+        driver = ogr.GetDriverByName('ESRI Shapefile')
+        if os.path.exists(out_filename):
+            # Delete the detination file first
+            driver.DeleteDataSource(out_filename)
+
+        self._ds = driver.CreateDataSource(out_filename)
+        self._layer = None
+
+    def _add_feature(self, feature):
+        f = ogr.Feature(feature_def=self._layer.GetLayerDefn())
+        f.SetGeometryDirectly(feature)
+        self._layer.CreateFeature(f)
+        f.Destroy()
+
+    def flush(self):
+        """
+        Commit the file to disk and prevent any further addition of
+        new longitude/latitude lines
+        """
+        self._ds.Destroy()
+        self._ds = None
+
+    def get_layer_name(self):
+        """Returns the name of the layer used for this shape file."""
+        return self._layer_name
+
+    def get_filepath(self):
+        """Returns the path to the destination shape file."""
+        return self._filepath
+
+    def __str__(self):
+        return "ShapeFile(%s)" % self._filepath
+
+class LineShapeFile(_ShapeFile):
+    """
+    Shape file for LineString geometries.
+    """
+
+    def __init__(self, bounding_box, out_filename, layer_name):
+        _ShapeFile.__init__(self, bounding_box, out_filename, layer_name)
+        self._layer = self._ds.CreateLayer(self._layer_name,
+                                           geom_type=ogr.wkbLineString)
+        l.debug('Created layer %s in LineShapeFile %s.' %
+                (layer_name, out_filename))
+
+    def add_bounding_rectangle(self):
+        self.add_horiz_line(self._bbox.get_top_left()[0])
+        self.add_horiz_line(self._bbox.get_bottom_right()[0])
+        self.add_vert_line(self._bbox.get_top_left()[1])
+        self.add_vert_line(self._bbox.get_bottom_right()[1])
+        return self
+
+    def add_horiz_line(self, y):
+        """Add a new latitude line at the given latitude."""
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(self._bbox.get_top_left()[1], y)
+        line.AddPoint_2D(self._bbox.get_bottom_right()[1], y)
+        self._add_feature(line)
+        return self
+
+    def add_vert_line(self, x):
+        """Add a new longitude line at the given longitude."""
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(x, self._bbox.get_top_left()[0])
+        line.AddPoint_2D(x, self._bbox.get_bottom_right()[0])
+        self._add_feature(line)
+        return self
+
+class BoxShapeFile(LineShapeFile):
+    """
+    Shape file for Box geometries.
+    """
+
+    def add_box(self, box):
+        top_left, bottom_right = box.get_top_left(), box.get_bottom_right()
+
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(*list(reversed(top_left)))
+        line.AddPoint_2D(bottom_right[1], top_left[0])
+        self._add_feature(line)
+
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(bottom_right[1], top_left[0])
+        line.AddPoint_2D(*list(reversed(bottom_right)))
+        self._add_feature(line)
+
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(*list(reversed(bottom_right)))
+        line.AddPoint_2D(top_left[1], bottom_right[0])
+        self._add_feature(line)
+
+        line = ogr.Geometry(type = ogr.wkbLineString)
+        line.AddPoint_2D(top_left[1], bottom_right[0])
+        line.AddPoint_2D(*list(reversed(top_left)))
+        self._add_feature(line)
+        return self
+
+class PolyShapeFile(_ShapeFile):
+    """
+    Shape file for Polygon geometries.
+    """
+
+    def __init__(self, bounding_box, out_filename, layer_name):
+        _ShapeFile.__init__(self, bounding_box, out_filename, layer_name)
+        self._layer = self._ds.CreateLayer(self._layer_name,
+                                           geom_type=ogr.wkbPolygon)
+        l.debug('Created layer %s in PolyShapeFile %s.' %
+                (layer_name, out_filename))
+
+    def add_shade_from_wkt(self, wkt):
+        """Add the polygon feature to the shape file."""
+        # Prevent the current locale from influencing how the WKT data is
+        # parsed by OGR.
+        prev_locale = locale.getlocale(locale.LC_ALL)
+        locale.setlocale(locale.LC_ALL, "C")
+
+        try:
+            poly = ogr.CreateGeometryFromWkt(wkt)
+        finally:
+            locale.setlocale(locale.LC_ALL, prev_locale)
+
+        self._add_feature(poly)
+        return self
+
+if __name__ == "__main__":
+    from ocitysmap2 import coords
+
+    logging.basicConfig(level=logging.DEBUG)
+    (LineShapeFile(coords.BoundingBox(44.4883, -1.0901, 44.4778, -1.0637),
+                   '/tmp/mygrid.shp', 'test')
+        .add_horiz_line(44.48)
+        .add_vert_line(-1.08)
+        .flush())
diff --git a/ocitysmap2-render b/ocitysmap2-render
deleted file mode 100755
index 5d615cd..0000000
--- a/ocitysmap2-render
+++ /dev/null
@@ -1,241 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8; mode: Python -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2009  David Decotigny
-# Copyright (C) 2009  Frédéric Lehobey
-# Copyright (C) 2009  David Mentré
-# Copyright (C) 2009  Maxime Petazzoni
-# Copyright (C) 2009  Thomas Petazzoni
-# Copyright (C) 2009  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-__version__ = '0.1'
-
-import logging
-import optparse
-import sys, os
-
-import ocitysmap2
-import ocitysmap2.layoutlib.renderers
-from coords import BoundingBox
-
-def main():
-    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
-
-    # Paper sizes, sorted in increasing widths
-    KNOWN_PAPER_SIZE_NAMES = \
-        map(lambda p: p[0],
-            sorted(ocitysmap2.layoutlib.PAPER_SIZES,
-                   key=lambda p: p[1]))
-
-    # Known renderer names
-    KNOWN_RENDERERS_NAMES = \
-        map(lambda r: "%s (%s)" % (r.name, r.description),
-            ocitysmap2.layoutlib.renderers.get_renderers())
-
-    # Known paper orientations
-    KNOWN_PAPER_ORIENTATIONS = ['portrait', 'landscape']
-
-    usage = '%prog [options] [-b <lat1,long1 lat2,long2>|--osmid <osmid>]'
-    parser = optparse.OptionParser(usage=usage,
-                                   version='%%prog %s' % __version__)
-    parser.add_option('-C', '--config', dest='config_file', metavar='FILE',
-                      help='Specify the location of the config file.')
-    parser.add_option('-p', '--prefix', dest='output_prefix', metavar='PREFIX',
-                      help='Specify the prefix of generated files. '
-                           'Defaults to "citymap".',
-                      default='citymap')
-    parser.add_option('-f', '--format', dest='output_formats', metavar='FMT',
-                      help='Specify the output formats. Supported file '
-                           'formats: svg, svgz, pdf, ps, ps.gz, png, and csv. '
-                           'Defaults to PDF. May be specified multiple times.',
-                      action='append')
-    parser.add_option('-t', '--title', dest='output_title', metavar='TITLE',
-                      help='Specify the title displayed in the output files.',
-                      default="My Map")
-    parser.add_option('--osmid', dest='osmid', metavar='OSMID',
-                      help='OSM id representing the polygon of the city '
-                      'to render.', type="int"),
-    parser.add_option('-b', '--bounding-box', dest='bbox',  nargs=2,
-                      metavar='LAT1,LON1 LAT2,LON2',
-                      help='Bounding box (EPSG: 4326).')
-    parser.add_option('-L', '--language', dest='language',
-                      metavar='LANGUAGE_CODE',
-                      help='Language to use when generating the index'
-                           ' (default=fr_FR.UTF-8).',
-                      default='fr_FR.UTF-8')
-    parser.add_option('-s', '--stylesheet', dest='stylesheet',
-                      metavar='NAME',
-                      help="Name of the stylesheet to use. Defaults to the "
-                      "first specified in the config file.")
-    parser.add_option('-l', '--layout', dest='layout',
-                      metavar='NAME',
-                      default=KNOWN_RENDERERS_NAMES[0].split()[0],
-                      help= ("Name of the layout to use, among %s. Default: 
%s."
-                             % (', '.join(KNOWN_RENDERERS_NAMES),
-                                KNOWN_RENDERERS_NAMES[0].split()[0])))
-    parser.add_option('--paper-format', metavar='FMT',
-                      help='Either "default", or one of %s.'\
-                          % ', '.join(KNOWN_PAPER_SIZE_NAMES),
-                      default='default')
-    parser.add_option('--orientation', metavar='ORIENTATION',
-                      help='Either "portrait" or "landscape".',
-                      default='portrait')
-
-    (options, args) = parser.parse_args()
-    if len(args):
-        parser.print_help()
-        return 1
-
-    # Make sure either -b or -c is given
-    optcnt = 0
-    for var in options.bbox, options.osmid:
-        if var:
-            optcnt += 1
-
-    if optcnt == 0:
-        parser.error("One of --bounding-box "
-                     "or --osmid is mandatory")
-
-    if optcnt > 1:
-        parser.error("Options --bounding-box "
-                     "or --osmid are exclusive")
-
-    # Parse config file and instanciate main object
-    mapper = ocitysmap2.OCitySMap([options.config_file
-                                   or os.path.join(os.environ["HOME"],
-                                                   '.ocitysmap.conf')])
-
-    # Parse bounding box arguments when given
-    bbox = None
-    if options.bbox:
-        try:
-            bbox = BoundingBox.parse_latlon_strtuple(options.bbox)
-        except ValueError:
-            parser.error('Invalid bounding box!')
-        # Check that latitude and langitude are different
-        lat1, lon1 = bbox.get_top_left()
-        lat2, lon2 = bbox.get_bottom_right()
-        if lat1 == lat2:
-            parser.error('Same latitude in bounding box corners')
-        if lon1 == lon2:
-            parser.error('Same longitude in bounding box corners')
-
-    # Parse OSM id when given
-    if options.osmid:
-        try:
-            bbox  = BoundingBox.parse_wkt(
-                mapper.get_geographic_info(options.osmid)[0])
-        except LookupError:
-            parser.error('No such OSM id: %d' % options.osmid)
-
-    # Parse stylesheet (defaults to 1st one)
-    if options.stylesheet is None:
-        stylesheet = mapper.get_all_style_configurations()[0]
-    else:
-        try:
-            stylesheet = mapper.get_stylesheet_by_name(options.stylesheet)
-        except LookupError, ex:
-            parser.error("%s. Available stylesheets: %s."
-                 % (ex, ', '.join(map(lambda s: "%s (%s)"
-                          % (s.name, s.description),
-                          mapper.STYLESHEET_REGISTRY))))
-
-    # Parse rendering layout
-    if options.layout is None:
-        cls_renderer = ocitysmap2.layoutlib.renderers.get_renderers()[0]
-    else:
-        try:
-            cls_renderer = 
ocitysmap2.layoutlib.renderers.get_renderer_class_by_name(options.layout)
-        except LookupError, ex:
-            parser.error("%s\nAvailable layouts: %s."
-                 % (ex, ', '.join(map(lambda lo: "%s (%s)"
-                          % (lo.name, lo.description),
-                          ocitysmap2.layoutlib.renderers.get_renderers()))))
-
-    # Output file formats
-    if not options.output_formats:
-        options.output_formats = ['pdf']
-    options.output_formats = set(options.output_formats)
-
-    # Reject output formats that are not supported by the renderer
-    compatible_output_formats = cls_renderer.get_compatible_output_formats()
-    for format in options.output_formats:
-        if format not in compatible_output_formats:
-            parser.error("Output format %s not supported by layout %s" %
-                         (format, cls_renderer.name))
-
-    # Parse paper size
-    if (options.paper_format != 'default') \
-            and options.paper_format not in KNOWN_PAPER_SIZE_NAMES:
-        parser.error("Invalid paper format. Allowed formats = default, %s"
-                     % ', '.join(KNOWN_PAPER_SIZE_NAMES))
-
-    # Determine actual paper size
-    compat_papers = cls_renderer.get_compatible_paper_sizes(bbox)
-    if not compat_papers:
-        parser.error("No paper size compatible with this rendering.")
-
-    paper_descr = None
-    if options.paper_format == 'default':
-        for p in compat_papers:
-            if p[5]:
-                paper_descr = p
-                break
-    else:
-        # Make sure the requested paper size is in list
-        for p in compat_papers:
-            if p[0] == options.paper_format:
-                paper_descr = p
-                break
-    if not paper_descr:
-        parser.error("Requested paper format not compatible with rendering. 
Compatible paper formats are: %s."
-             % ', '.join(map(lambda p: "%s (%.1fx%.1fcm²)"
-                % (p[0], p[1]/10., p[2]/10.),
-                compat_papers)))
-    assert paper_descr[3] or paper_descr[4] # Portrait or Landscape accepted
-
-    # Validate requested orientation
-    if options.orientation not in KNOWN_PAPER_ORIENTATIONS:
-        parser.error("Invalid paper orientation. Allowed orientations: %s"
-                     % KNOWN_PAPER_ORIENTATIONS)
-
-    if (options.orientation == 'portrait' and not paper_descr[3]) or \
-        (options.orientation == 'landscape' and not paper_descr[4]):
-        parser.error("Requested paper orientation %s not compatible with this 
rendering at this paper size." % options.orientation)
-
-    # Prepare the rendering config
-    rc              = ocitysmap2.RenderingConfiguration()
-    rc.title        = options.output_title
-    rc.osmid        = options.osmid or None # Force to None if absent
-    rc.bounding_box = bbox
-    rc.language     = options.language
-    rc.stylesheet   = stylesheet
-    if options.orientation == 'portrait':
-        rc.paper_width_mm  = paper_descr[1]
-        rc.paper_height_mm = paper_descr[2]
-    else:
-        rc.paper_width_mm  = paper_descr[2]
-        rc.paper_height_mm = paper_descr[1]
-
-    # Go !...
-    mapper.render(rc, cls_renderer.name, options.output_formats,
-                  options.output_prefix)
-
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
diff --git a/ocitysmap2.conf-template b/ocitysmap2.conf-template
deleted file mode 100644
index 1681be1..0000000
--- a/ocitysmap2.conf-template
+++ /dev/null
@@ -1,24 +0,0 @@
-[datasource]
-host=localhost
-user=maposmatic
-password=mysecurepasswd
-dbname=maposmatic
-# Optional database port, defaults to 5432
-# port=5432
-
-[rendering]
-# List of available stylesheets, each needs to be described by an eponymous
-# configuration section in this file.
-available_stylesheets: stylesheet_osm1, stylesheet_osm2
-
-# The default Mapnik stylesheet.
-[stylesheet_osm1]
-name: Default
-description: The default OSM style
-path: /path/to/mapnik-osm/osm.xml
-
-# Another stylesheet
-[stylesheet_osm2]
-name: AnotherOne
-description: Another OSM Stylesheet
-path: /path/to/another/osm.xml
diff --git a/ocitysmap2/.gitignore b/ocitysmap2/.gitignore
deleted file mode 100644
index 0d20b64..0000000
--- a/ocitysmap2/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.pyc
diff --git a/ocitysmap2/__init__.py b/ocitysmap2/__init__.py
deleted file mode 100644
index e2c60df..0000000
--- a/ocitysmap2/__init__.py
+++ /dev/null
@@ -1,557 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-"""OCitySMap 2.
-
-OCitySMap is a Mapnik-based map rendering engine from OpenStreetMap.org data.
-It is architectured around the concept of Renderers, in charge of rendering the
-map and all the visual features that go along with it (scale, grid, legend,
-index, etc.) on the given paper size using a provided Mapnik stylesheet,
-according to their implemented layout.
-
-The PlainRenderer for example renders a full-page map with its grid, a title
-header and copyright notice, but without the index.
-
-How to use OCitySMap?
----------------------
-
-The API of OCitySMap is very simple. First, you need to instanciate the main
-OCitySMap class with the path to your OCitySMap configuration file (see
-ocitysmap.conf-template):
-
-
-    ocitysmap = ocitysmap2.OCitySMap('/path/to/your/config')
-
-The next step is to create a RenderingConfiguration, the object that
-encapsulates all the information to parametize the rendering, including the
-Mapnik stylesheet. You can retrieve the list of supported stylesheets (directly
-as Stylesheet objects) with:
-
-    styles = ocitysmap.get_all_style_configurations()
-
-Fill in your RenderingConfiguration with the map title, the OSM ID or bounding
-box, the chosen map language, the Stylesheet object and the paper size (in
-millimeters) and simply pass it to OCitySMap's render method:
-
-    ocitysmap.render(rendering_configuration, layout_name,
-                     output_formats, prefix)
-
-The layout name is the renderer's key name. You can get the list of all
-supported renderers with ocitysmap.get_all_renderers(). The output_formats is a
-list of output formats. For now, the following formats are supported:
-
-    * PNG at 72dpi
-    * PDF
-    * SVG
-    * SVGZ (gzipped-SVG)
-    * PS
-
-The prefix is the filename prefix for all the rendered files. This is usually a
-path to the destination's directory, eventually followed by some unique, yet
-common prefix for the files rendered for a job.
-"""
-
-__author__ = 'The MapOSMatic developers'
-__version__ = '0.2'
-
-import cairo
-import ConfigParser
-import gzip
-import logging
-import os
-import psycopg2
-import re
-import tempfile
-
-import shapely
-import shapely.wkt
-import shapely.geometry
-
-import coords
-import i18n
-
-from indexlib.indexer import StreetIndex
-from indexlib.commons import IndexDoesNotFitError, IndexEmptyError
-
-from layoutlib import PAPER_SIZES, renderers
-import layoutlib.commons
-
-LOG = logging.getLogger('ocitysmap')
-
-class RenderingConfiguration:
-    """
-    The RenderingConfiguration class encapsulate all the information concerning
-    a rendering request. This data is used by the layout renderer, in
-    conjonction with its rendering mode (defined by its implementation), to
-    produce the map.
-    """
-
-    def __init__(self):
-        self.title           = None # str
-        self.osmid           = None # None / int (shading + city name)
-        self.bounding_box    = None # bbox (from osmid if None)
-        self.language        = None # str (locale)
-
-        self.stylesheet      = None # Obj Stylesheet
-
-        self.paper_width_mm  = None
-        self.paper_height_mm = None
-
-        # Setup by OCitySMap::render() from osmid and bounding_box fields:
-        self.polygon_wkt     = None # str (WKT of interest)
-
-        # Setup by OCitySMap::render() from language field:
-        self.i18n            = None # i18n object
-
-
-class Stylesheet:
-    """
-    A Stylesheet object defines how the map features will be rendered. It
-    contains information pointing to the Mapnik stylesheet and other styling
-    parameters.
-    """
-    DEFAULT_ZOOM_LEVEL = 16
-
-    def __init__(self):
-        self.name        = None # str
-        self.path        = None # str
-        self.description = '' # str
-
-        self.grid_line_color = 'black'
-        self.grid_line_alpha = 0.5
-        self.grid_line_width = 1
-
-        self.shade_color = 'black'
-        self.shade_alpha = 0.1
-
-        # shade color for town contour in multi-pages
-        self.shade_color_2 = 'white'
-        self.shade_alpha_2 = 0.4
-
-    @staticmethod
-    def create_from_config_section(parser, section_name):
-        """Creates a Stylesheet object from the OCitySMap configuration.
-
-        Args:
-            parser (ConfigParser.ConfigParser): the configuration parser
-                object.
-            section_name (string): the stylesheet section name in the
-                configuration.
-        """
-        s = Stylesheet()
-
-        def assign_if_present(key, cast_fn=str):
-            if parser.has_option(section_name, key):
-                setattr(s, key, cast_fn(parser.get(section_name, key)))
-
-        s.name = parser.get(section_name, 'name')
-        s.path = parser.get(section_name, 'path')
-        assign_if_present('description')
-
-        assign_if_present('grid_line_color')
-        assign_if_present('grid_line_alpha', float)
-        assign_if_present('grid_line_width', int)
-
-        assign_if_present('shade_color')
-        assign_if_present('shade_alpha', float)
-
-        assign_if_present('shade_color_2')
-        assign_if_present('shade_alpha_2', float)
-        return s
-
-    @staticmethod
-    def create_all_from_config(parser):
-        styles = parser.get('rendering', 'available_stylesheets')
-        if not styles:
-            raise ValueError, \
-                    'OCitySMap configuration does not contain any stylesheet!'
-
-        return [Stylesheet.create_from_config_section(parser, name.strip())
-                for name in styles.split(',')]
-
-class OCitySMap:
-    """
-    This is the main entry point of the OCitySMap map rendering engine. Read
-    this module's documentation for more details on its API.
-    """
-
-    DEFAULT_REQUEST_TIMEOUT_MIN = 15
-
-    DEFAULT_RENDERING_PNG_DPI = 72
-
-    STYLESHEET_REGISTRY = []
-
-    def __init__(self, config_files=None):
-        """Instanciate a new configured OCitySMap instance.
-
-        Args:
-            config_file (string or list or None): path, or list of paths to
-                the OCitySMap configuration file(s). If None, sensible defaults
-                are tried.
-        """
-
-        if config_files is None:
-            config_files = ['/etc/ocitysmap.conf', '~/.ocitysmap.conf']
-        elif not isinstance(config_files, list):
-            config_files = [config_files]
-
-        config_files = map(os.path.expanduser, config_files)
-        LOG.info('Reading OCitySMap configuration from %s...' %
-                 ', '.join(config_files))
-
-        self._parser = ConfigParser.RawConfigParser()
-        if not self._parser.read(config_files):
-            raise IOError, 'None of the configuration files could be read!'
-
-        self._locale_path = os.path.join(os.path.dirname(__file__), '..', 
'locale')
-        self.__db = None
-
-        # Read stylesheet configuration
-        self.STYLESHEET_REGISTRY = 
Stylesheet.create_all_from_config(self._parser)
-        LOG.debug('Found %d Mapnik stylesheets.'
-                  % len(self.STYLESHEET_REGISTRY))
-
-    @property
-    def _db(self):
-        if self.__db:
-            return self.__db
-
-        # Database connection
-        datasource = dict(self._parser.items('datasource'))
-        # The port is not a mandatory configuration option, so make
-        # sure we define a default value.
-        if not datasource.has_key('port'):
-            datasource['port'] = 5432
-        LOG.info('Connecting to database %s on %s:%s as %s...' %
-                 (datasource['dbname'], datasource['host'], datasource['port'],
-                  datasource['user']))
-
-        db = psycopg2.connect(user=datasource['user'],
-                              password=datasource['password'],
-                              host=datasource['host'],
-                              database=datasource['dbname'],
-                              port=datasource['port'])
-
-        # Force everything to be unicode-encoded, in case we run along Django
-        # (which loads the unicode extensions for psycopg2)
-        db.set_client_encoding('utf8')
-
-        # Make sure the DB is correctly installed
-        self._verify_db(db)
-
-        try:
-            timeout = int(self._parser.get('datasource', 'request_timeout'))
-        except (ConfigParser.NoOptionError, ValueError):
-            timeout = OCitySMap.DEFAULT_REQUEST_TIMEOUT_MIN
-        self._set_request_timeout(db, timeout)
-
-        self.__db = db
-        return self.__db
-
-    def _verify_db(self, db):
-        """Make sure the PostGIS DB is compatible with us."""
-        cursor = db.cursor()
-        cursor.execute("""
-SELECT ST_AsText(ST_LongestLine(
-                    'POINT(100 100)'::geometry,
-                   'LINESTRING(20 80, 98 190, 110 180, 50 75 )'::geometry)
-               ) As lline;
-""")
-        assert cursor.fetchall()[0][0] == "LINESTRING(100 100,98 190)", \
-            LOG.fatal("PostGIS >= 1.5 required for correct operation !")
-
-    def _set_request_timeout(self, db, timeout_minutes=15):
-        """Sets the PostgreSQL request timeout to avoid long-running queries on
-        the database."""
-        cursor = db.cursor()
-        cursor.execute('set session statement_timeout=%d;' %
-                       (timeout_minutes * 60 * 1000))
-        cursor.execute('show statement_timeout;')
-        LOG.debug('Configured statement timeout: %s.' %
-                  cursor.fetchall()[0][0])
-
-    def _cleanup_tempdir(self, tmpdir):
-        LOG.debug('Cleaning up %s...' % tmpdir)
-        for root, dirs, files in os.walk(tmpdir, topdown=False):
-            for name in files:
-                os.remove(os.path.join(root, name))
-            for name in dirs:
-                os.rmdir(os.path.join(root, name))
-        os.rmdir(tmpdir)
-
-    def _get_geographic_info(self, osmid, table):
-        """Return the area for the given osm id in the given table, or raise
-        LookupError when not found
-
-        Args:
-            osmid (integer): OSM ID
-            table (str): either 'polygon' or 'line'
-
-        Return:
-            Geos geometry object
-        """
-
-        # Ensure all OSM IDs are integers, bust cast them back to strings
-        # afterwards.
-        LOG.debug('Looking up bounding box and contour of OSM ID %d...'
-                  % osmid)
-
-        cursor = self._db.cursor()
-        cursor.execute("""select
-                            st_astext(st_transform(st_buildarea(st_union(way)),
-                                                   4002))
-                          from planet_osm_%s where osm_id = %d
-                          group by osm_id;""" %
-                       (table, osmid))
-        records = cursor.fetchall()
-        try:
-            ((wkt,),) = records
-            if wkt is None:
-                raise ValueError
-        except ValueError:
-            raise LookupError("OSM ID %d not found in table %s" %
-                              (osmid, table))
-
-        return shapely.wkt.loads(wkt)
-
-    def get_geographic_info(self, osmid):
-        """Return a tuple (WKT_envelope, WKT_buildarea) or raise
-        LookupError when not found
-
-        Args:
-            osmid (integer): OSM ID
-
-        Return:
-            tuple (WKT bbox, WKT area)
-        """
-        found = False
-
-        # Scan polygon table:
-        try:
-            polygon_geom = self._get_geographic_info(osmid, 'polygon')
-            found = True
-        except LookupError:
-            polygon_geom = shapely.geometry.Polygon()
-
-        # Scan line table:
-        try:
-            line_geom = self._get_geographic_info(osmid, 'line')
-            found = True
-        except LookupError:
-            line_geom = shapely.geometry.Polygon()
-
-        # Merge results:
-        if not found:
-            raise LookupError("No such OSM id: %d" % osmid)
-
-        result = polygon_geom.union(line_geom)
-        return (result.envelope.wkt, result.wkt)
-
-    def get_osm_database_last_update(self):
-        cursor = self._db.cursor()
-        query = "select last_update from maposmatic_admin;"
-        try:
-            cursor.execute(query)
-        except psycopg2.ProgrammingError:
-            self._db.rollback()
-            return None
-        # Extract datetime object. It is located as the first element
-        # of a tuple, itself the first element of an array.
-        return cursor.fetchall()[0][0]
-
-    def get_all_style_configurations(self):
-        """Returns the list of all available stylesheet configurations (list of
-        Stylesheet objects)."""
-        return self.STYLESHEET_REGISTRY
-
-    def get_stylesheet_by_name(self, name):
-        """Returns a stylesheet by its key name."""
-        for style in self.STYLESHEET_REGISTRY:
-            if style.name == name:
-                return style
-        raise LookupError, 'The requested stylesheet %s was not found!' % name
-
-    def get_all_renderers(self):
-        """Returns the list of all available layout renderers (list of
-        Renderer classes)."""
-        return renderers.get_renderers()
-
-    def get_all_paper_sizes(self):
-        return PAPER_SIZES
-
-    def render(self, config, renderer_name, output_formats, file_prefix):
-        """Renders a job with the given rendering configuration, using the
-        provided renderer, to the given output formats.
-
-        Args:
-            config (RenderingConfiguration): the rendering configuration
-                object.
-            renderer_name (string): the layout renderer to use for this 
rendering.
-            output_formats (list): a list of output formats to render to, from
-                the list of supported output formats (pdf, svgz, etc.).
-            file_prefix (string): filename prefix for all output files.
-        """
-
-        assert config.osmid or config.bounding_box, \
-                'At least an OSM ID or a bounding box must be provided!'
-
-        output_formats = map(lambda x: x.lower(), output_formats)
-        config.i18n = i18n.install_translation(config.language,
-                                               self._locale_path)
-
-        LOG.info('Rendering with renderer %s in language: %s (rtl: %s).' %
-                 (renderer_name, config.i18n.language_code(),
-                  config.i18n.isrtl()))
-
-        # Determine bounding box and WKT of interest
-        if config.osmid:
-            osmid_bbox, osmid_area \
-                = self.get_geographic_info(config.osmid)
-
-            # Define the bbox if not already defined
-            if not config.bounding_box:
-                config.bounding_box \
-                    = coords.BoundingBox.parse_wkt(osmid_bbox)
-
-            # Update the polygon WKT of interest
-            config.polygon_wkt = osmid_area
-        else:
-            # No OSM ID provided => use specified bbox
-            config.polygon_wkt = config.bounding_box.as_wkt()
-
-        # Make sure we have a bounding box
-        assert config.bounding_box is not None
-        assert config.polygon_wkt is not None
-
-        osm_date = self.get_osm_database_last_update()
-
-        # Create a temporary directory for all our shape files
-        tmpdir = tempfile.mkdtemp(prefix='ocitysmap')
-        try:
-            LOG.debug('Rendering in temporary directory %s' % tmpdir)
-
-            # Prepare the generic renderer
-            renderer_cls = renderers.get_renderer_class_by_name(renderer_name)
-
-            # Perform the actual rendering to the Cairo devices
-            for output_format in output_formats:
-                output_filename = '%s.%s' % (file_prefix, output_format)
-                try:
-                    self._render_one(config, tmpdir, renderer_cls,
-                                     output_format, output_filename, osm_date,
-                                     file_prefix)
-                except IndexDoesNotFitError:
-                    LOG.exception("The actual font metrics probably don't "
-                                  "match those pre-computed by the renderer's"
-                                  "constructor. Backtrace follows...")
-        finally:
-            self._cleanup_tempdir(tmpdir)
-
-    def _render_one(self, config, tmpdir, renderer_cls,
-                    output_format, output_filename, osm_date, file_prefix):
-
-        LOG.info('Rendering to %s format...' % output_format.upper())
-
-        factory = None
-        dpi = layoutlib.commons.PT_PER_INCH
-
-        if output_format == 'png':
-            try:
-                dpi = int(self._parser.get('rendering', 'png_dpi'))
-            except ConfigParser.NoOptionError:
-                dpi = OCitySMap.DEFAULT_RENDERING_PNG_DPI
-
-            # As strange as it may seem, we HAVE to use a vector
-            # device here and not a raster device such as
-            # ImageSurface. Because, for some reason, with
-            # ImageSurface, the font metrics would NOT match those
-            # pre-computed by renderer_cls.__init__() and used to
-            # layout the whole page
-            def factory(w,h):
-                w_px = int(layoutlib.commons.convert_pt_to_dots(w, dpi))
-                h_px = int(layoutlib.commons.convert_pt_to_dots(h, dpi))
-                LOG.debug("Rendering PNG into %dpx x %dpx area..."
-                          % (w_px, h_px))
-                return cairo.PDFSurface(None, w_px, h_px)
-
-        elif output_format == 'svg':
-            factory = lambda w,h: cairo.SVGSurface(output_filename, w, h)
-        elif output_format == 'svgz':
-            factory = lambda w,h: cairo.SVGSurface(
-                    gzip.GzipFile(output_filename, 'wb'), w, h)
-        elif output_format == 'pdf':
-            factory = lambda w,h: cairo.PDFSurface(output_filename, w, h)
-        elif output_format == 'ps':
-            factory = lambda w,h: cairo.PSSurface(output_filename, w, h)
-        elif output_format == 'ps.gz':
-            factory = lambda w,h: cairo.PSSurface(
-                gzip.GzipFile(output_filename, 'wb'), w, h)
-        elif output_format == 'csv':
-            # We don't render maps into CSV.
-            return
-
-        else:
-            raise ValueError, \
-                'Unsupported output format: %s!' % output_format.upper()
-
-        renderer = renderer_cls(self._db, config, tmpdir, dpi, file_prefix)
-
-        surface = factory(renderer.paper_width_pt, renderer.paper_height_pt)
-
-        renderer.render(surface, dpi, osm_date)
-
-        LOG.debug('Writing %s...' % output_filename)
-        if output_format == 'png':
-            surface.write_to_png(output_filename)
-
-        surface.finish()
-
-if __name__ == '__main__':
-    logging.basicConfig(level=logging.DEBUG)
-
-    o = OCitySMap([os.path.join(os.path.dirname(__file__), '..',
-                                'ocitysmap.conf.mine')])
-
-    c = RenderingConfiguration()
-    c.title = 'Chevreuse, Yvelines, Île-de-France, France, Europe, Monde'
-    c.osmid = -943886 # Chevreuse
-    # c.osmid = -7444   # Paris
-    c.language = 'fr_FR.UTF-8'
-    c.paper_width_mm = 297
-    c.paper_height_mm = 420
-    c.stylesheet = o.get_stylesheet_by_name('Default')
-
-    # c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
-    o.render(c, 'single_page_index_bottom',
-             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
-             '/tmp/mymap_index_bottom')
-
-    c.paper_width_mm,c.paper_height_mm = c.paper_height_mm,c.paper_width_mm
-    o.render(c, 'single_page_index_side',
-             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
-             '/tmp/mymap_index_side')
-
-    o.render(c, 'plain',
-             ['png', 'pdf', 'ps.gz', 'svgz', 'csv'],
-             '/tmp/mymap_plain')
diff --git a/ocitysmap2/coords.py b/ocitysmap2/coords.py
deleted file mode 100644
index 32c1426..0000000
--- a/ocitysmap2/coords.py
+++ /dev/null
@@ -1,203 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import math
-
-import shapely.wkt
-
-# Importing mapnik2 raises a DeprecationWarning as of mapnik
-# commit 14700dba. As mapnik 2.1 (or git version with support for
-# placement-type="simple") is required for OCitySMap (see INSTALL),
-# instead of importing mapnik2, we import mapnik and assert it isn't
-# an old version.
-import mapnik
-assert mapnik.mapnik_version >= 200100, \
-    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
-    "for more details." % mapnik.mapnik_version_string()
-
-_MAPNIK_PROJECTION = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 " \
-                     "+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m   " \
-                     "address@hidden +no_defs +over"
-
-
-EARTH_RADIUS = 6370986 # meters
-
-
-class Point:
-    def __init__(self, lat, long_):
-        self._lat, self._long = float(lat), float(long_)
-
-    @staticmethod
-    def parse_wkt(wkt):
-        long_,lat = wkt[6:-1].split()
-        return Point(lat, long_)
-
-    def get_latlong(self):
-        return self._lat, self._long
-
-    def as_wkt(self, with_point_statement=True):
-        contents = '%f %f' % (self._long, self._lat)
-        if with_point_statement:
-            return "POINT(%s)" % contents
-        return contents
-
-    def __str__(self):
-        return 'Point(lat=%f, long_=%f)' % (self._lat, self._long)
-
-    def spheric_spherical_vector(self, other):
-        """Approx (self - other) vector converted to lat/long meters
-        wrt the given other point"""
-        delta_lat  = abs(self._lat - other._lat)
-        delta_long = abs(self._long - other._long)
-        radius_lat = EARTH_RADIUS * math.cos(math.radians(self._lat))
-        return (EARTH_RADIUS * math.radians(delta_lat),
-                radius_lat * math.radians(delta_long))
-
-
-class BoundingBox:
-    """
-    The BoundingBox class defines a geographic rectangle area specified by the
-    coordinates of its top left and bottom right corners, in latitude and
-    longitude (4002 projection).
-    """
-
-    def __init__(self, lat1, long1, lat2, long2):
-        (self._lat1, self._long1) = float(lat1), float(long1)
-        (self._lat2, self._long2) = float(lat2), float(long2)
-
-        # make sure lat1/long1 is the upper left, and the others the btm right
-        if (self._lat1 < self._lat2):
-            self._lat1, self._lat2 = self._lat2, self._lat1
-        if (self._long1 > self._long2):
-            self._long1, self._long2 = self._long2, self._long1
-
-    @staticmethod
-    def parse_wkt(wkt):
-        """Returns a BoundingBox object created from the coordinates of a
-        polygon given in WKT format."""
-        try:
-            geom_envelope = shapely.wkt.loads(wkt).bounds
-        except Exception, rx:
-            raise ValueError("Invalid input WKT: %s" % ex)
-        return BoundingBox(geom_envelope[1], geom_envelope[0],
-                           geom_envelope[3], geom_envelope[2])
-
-    @staticmethod
-    def parse_latlon_strtuple(points):
-        """Returns a BoundingBox object from a tuple of strings
-        [("lat1,lon1"), ("lat2,lon2")]"""
-        (lat1, long1) = points[0].split(',')
-        (lat2, long2) = points[1].split(',')
-        return BoundingBox(lat1, long1, lat2, long2)
-
-    def get_top_left(self):
-        return (self._lat1, self._long1)
-
-    def get_bottom_right(self):
-        return (self._lat2, self._long2)
-
-    def create_expanded(self, dlat, dlong):
-        """Return a new bbox of the same size + dlat/dlong added
-           on the top-left sides"""
-        return BoundingBox(self._lat1 + dlat, self._long1 - dlong,
-                           self._lat2 - dlat, self._long2 + dlong)
-
-    @staticmethod
-    def _ptstr(point):
-        return '%.4f,%.4f' % (point[0], point[1])
-
-    def __str__(self):
-        return 'BoundingBox(%s %s)' \
-            % (BoundingBox._ptstr(self.get_top_left()),
-               BoundingBox._ptstr(self.get_bottom_right()))
-
-    def as_wkt(self, with_polygon_statement=True):
-        xmax, ymin = self.get_top_left()
-        xmin, ymax = self.get_bottom_right()
-        s_coords = ("%f %f, %f %f, %f %f, %f %f, %f %f"
-                    % (ymin, xmin, ymin, xmax, ymax, xmax,
-                       ymax, xmin, ymin, xmin))
-        if with_polygon_statement:
-            return "POLYGON((%s))" % s_coords
-        return s_coords
-
-    def spheric_sizes(self):
-        """Metric distances at the bounding box top latitude.
-        Returns the tuple (metric_size_lat, metric_size_long)
-        """
-        delta_lat = abs(self._lat1 - self._lat2)
-        delta_long = abs(self._long1 - self._long2)
-        radius_lat = EARTH_RADIUS * math.cos(math.radians(self._lat1))
-        return (EARTH_RADIUS * math.radians(delta_lat),
-                radius_lat * math.radians(delta_long))
-
-    def get_pixel_size_for_zoom_factor(self, zoom):
-        """Return the size in pixels (tuple height,width) needed to
-        render the bounding box at the given zoom factor."""
-        delta_long = abs(self._long1 - self._long2)
-        # 2^zoom tiles (1 tile = 256 pix) for the whole earth
-        pix_x = delta_long * (2 ** (zoom + 8)) / 360
-
-        # http://en.wikipedia.org/wiki/Mercator_projection
-        yplan = lambda lat: math.log(math.tan(math.pi/4.0 +
-                                              math.radians(lat)/2.0))
-
-        # OSM maps are drawn between -85 deg and + 85, the whole amplitude
-        # is 256*2^(zoom)
-        pix_y = (yplan(self._lat1) - yplan(self._lat2)) \
-                * (2 ** (zoom + 7)) / yplan(85)
-
-        return (int(math.ceil(pix_y)), int(math.ceil(pix_x)))
-
-    def to_mercator(self):
-        envelope = mapnik.Box2d(self.get_top_left()[1],
-                                self.get_top_left()[0],
-                                self.get_bottom_right()[1],
-                                self.get_bottom_right()[0])
-        _proj = mapnik.Projection(_MAPNIK_PROJECTION)
-        bottom_left = _proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
-        top_right = _proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
-        top_left = mapnik.Coord(bottom_left.x, top_right.y)
-        bottom_right = mapnik.Coord(top_right.x, bottom_left.y)
-        return (bottom_right, bottom_left, top_left, top_right)
-
-    def as_javascript(self, name=None, color=None):
-        if name:
-            name_str = ", \"%s\"" % name
-        else:
-            name_str = ""
-
-        if color:
-            color_str = ", { color: \"%s\" }" % color
-        else:
-            color_str = ""
-
-        return 'BoundingBox(%f,%f,%f,%f%s%s)' % \
-            (self._lat1, self._long1, self._lat2, self._long2,
-             name_str, color_str)
-
-if __name__ == "__main__":
-    wkt = 'POINT(2.0333 48.7062132250362)'
-    pt = Point.parse_wkt(wkt)
-    print wkt, pt, pt.as_wkt()
diff --git a/ocitysmap2/draw_utils.py b/ocitysmap2/draw_utils.py
deleted file mode 100644
index 2b26562..0000000
--- a/ocitysmap2/draw_utils.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2012  David Decotigny
-# Copyright (C) 2012  Frédéric Lehobey
-# Copyright (C) 2012  Pierre Mauduit
-# Copyright (C) 2012  David Mentré
-# Copyright (C) 2012  Maxime Petazzoni
-# Copyright (C) 2012  Thomas Petazzoni
-# Copyright (C) 2012  Gaël Utard
-# Copyright (C) 2012  Étienne Loks
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import cairo
-import pango
-import pangocairo
-import ocitysmap2.layoutlib.commons as commons
-
-def draw_text(ctx, pc, layout, fascent, fheight,
-              baseline_x, baseline_y, text, pango_alignment):
-    """Draws the given text into the provided Cairo
-    context through the Pango layout (get_width() expected to be
-    correct in order to position the text correctly) with the
-    specified pango.ALIGN_x alignment.
-
-    Args:
-        ctx (cairo.Context): cairo context to use
-        pc (pangocairo.CairoContext): pango context
-        layout (pango.Layout): pango layout to draw into (get_with() important)
-        fascent, fheight (int): current font ascent/height metrics
-        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
-        pango_alignment (enum): pango.ALIGN_ constant value
-
-    Results:
-        A 3-uple text_width, text_height (cairo units)
-    """
-    layout.set_auto_dir(False) # Make sure ALIGN_RIGHT is independent on RTL...
-    layout.set_alignment(pango_alignment)
-    layout.set_text(text)
-    width, height = [x/pango.SCALE for x in layout.get_size()]
-
-    ctx.move_to(baseline_x, baseline_y - fascent)
-    pc.show_layout(layout)
-    return width, height
-
-def draw_text_left(ctx, pc, layout, fascent, fheight,
-                    baseline_x, baseline_y, text):
-    """Draws the given text left aligned into the provided Cairo
-    context through the Pango layout (get_width() expected to be
-    correct in order to position the text correctly).
-
-    Args:
-        ctx (cairo.Context): cairo context to use
-        pc (pangocairo.CairoContext): pango context
-        layout (pango.Layout): pango layout to draw into (get_with() important)
-        fascent, fheight (int): current font ascent/height metrics
-        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
-        pango_alignment (enum): pango.ALIGN_ constant value
-
-    Results:
-        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
-    """
-    w,h = draw_text(ctx, pc, layout, fascent, fheight,
-                    baseline_x, baseline_y, text, pango.ALIGN_LEFT)
-    return baseline_x, baseline_y, baseline_x + w
-
-def draw_text_center(ctx, pc, layout, fascent, fheight,
-                     baseline_x, baseline_y, text):
-    """Draws the given text centered inside the provided Cairo
-    context through the Pango layout (get_width() expected to be
-    correct in order to position the text correctly).
-
-    Args:
-        ctx (cairo.Context): cairo context to use
-        pc (pangocairo.CairoContext): pango context
-        layout (pango.Layout): pango layout to draw into (get_with() important)
-        fascent, fheight (int): current font ascent/height metrics
-        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
-        pango_alignment (enum): pango.ALIGN_ constant value
-
-    Results:
-        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
-    """
-    txt_width, txt_height = draw_text(ctx, pc, layout, fascent, fheight,
-                                      baseline_x, baseline_y, text,
-                                      pango.ALIGN_CENTER)
-    layout_width = layout.get_width() / pango.SCALE
-    return ( baseline_x + (layout_width - txt_width) / 2.,
-             baseline_y,
-             baseline_x + (layout_width + txt_width) / 2. )
-
-def draw_text_right(ctx, pc, layout, fascent, fheight,
-                    baseline_x, baseline_y, text):
-    """Draws the given text right aligned into the provided Cairo
-    context through the Pango layout (get_width() expected to be
-    correct in order to position the text correctly).
-
-    Args:
-        ctx (cairo.Context): cairo context to use
-        pc (pangocairo.CairoContext): pango context
-        layout (pango.Layout): pango layout to draw into (get_with() important)
-        fascent, fheight (int): current font ascent/height metrics
-        baseline_x/baseline_y (int): coordinate of the left baseline cairo 
point
-        pango_alignment (enum): pango.ALIGN_ constant value
-
-    Results:
-        A 3-uple left_x, baseline_y, right_x of the text rendered (cairo units)
-    """
-    txt_width, txt_height = draw_text(ctx, pc, layout, fascent, fheight,
-                                      baseline_x, baseline_y,
-                                      text, pango.ALIGN_RIGHT)
-    layout_width = layout.get_width() / pango.SCALE
-    return (baseline_x + layout_width - txt_width,
-            baseline_y,
-            baseline_x + layout_width)
-
-def draw_simpletext_center(ctx, text, x, y):
-    """
-    Draw the given text centered at x,y.
-
-    Args:
-       ctx (cairo.Context): The cairo context to use to draw.
-       text (str): the text to draw.
-       x,y (numbers): Location of the center (cairo units).
-    """
-    ctx.save()
-    xb, yb, tw, th, xa, ya = ctx.text_extents(text)
-    ctx.move_to(x - tw/2.0 - xb, y - yb/2.0)
-    ctx.show_text(text)
-    ctx.stroke()
-    ctx.restore()
-
-def draw_dotted_line(ctx, line_width, baseline_x, baseline_y, length):
-    ctx.set_line_width(line_width)
-    ctx.set_dash([line_width, line_width*2])
-    ctx.move_to(baseline_x, baseline_y)
-    ctx.rel_line_to(length, 0)
-    ctx.stroke()
-
-def adjust_font_size(layout, fd, constraint_x, constraint_y):
-    """
-    Grow the given font description (20% by 20%) until it fits in
-    designated area and then draw it.
-
-    Args:
-       layout (pango.Layout): The text block parameters.
-       fd (pango.FontDescriptor): The font object.
-       constraint_x/constraint_y (numbers): The area we want to
-           write into (cairo units).
-    """
-    while (layout.get_size()[0] / pango.SCALE < constraint_x and
-           layout.get_size()[1] / pango.SCALE < constraint_y):
-        fd.set_size(int(fd.get_size()*1.2))
-        layout.set_font_description(fd)
-    fd.set_size(int(fd.get_size()/1.2))
-    layout.set_font_description(fd)
-
-def draw_text_adjusted(ctx, text, x, y, width, height, max_char_number=None,
-                       text_color=(0, 0, 0, 1), align=pango.ALIGN_CENTER,
-                       width_adjust=0.7, height_adjust=0.8):
-    """
-    Draw a text adjusted to a maximum character number
-
-    Args:
-       ctx (cairo.Context): The cairo context to use to draw.
-       text (str): the text to draw.
-       x/y (numbers): The position on the canvas.
-       width/height (numbers): The area we want to
-           write into (cairo units).
-       max_char_number (number): If set a maximum character number.
-    """
-    pc = pangocairo.CairoContext(ctx)
-    layout = pc.create_layout()
-    layout.set_width(int(width_adjust * width * pango.SCALE))
-    layout.set_alignment(align)
-    fd = pango.FontDescription("Georgia Bold")
-    fd.set_size(pango.SCALE)
-    layout.set_font_description(fd)
-
-    if max_char_number:
-        # adjust size with the max character number
-        layout.set_text('0'*max_char_number)
-        adjust_font_size(layout, fd, width_adjust*width, height_adjust*height)
-
-    # set the real text
-    layout.set_text(text)
-    if not max_char_number:
-        adjust_font_size(layout, fd, width_adjust*width, height_adjust*height)
-
-    # draw
-    text_x, text_y, text_w, text_h = layout.get_extents()[1]
-    ctx.save()
-    ctx.set_source_rgba(*text_color)
-    if align == pango.ALIGN_CENTER:
-        x = x - (text_w/2.0)/pango.SCALE - int(float(text_x)/pango.SCALE)
-        y = y - (text_h/2.0)/pango.SCALE - int(float(text_y)/pango.SCALE)
-    else:
-        y = y - (text_h/2.0)/pango.SCALE - text_y/pango.SCALE
-    ctx.translate(x, y)
-
-    if align == pango.ALIGN_LEFT:
-        # Hack to workaround what appears to be a Cairo bug: without
-        # drawing a rectangle here, the translation above is not taken
-        # into account for rendering the text.
-        ctx.rectangle(0, 0, 0, 0)
-    pc.show_layout(layout)
-    ctx.restore()
-
-def render_page_number(ctx, page_number,
-                       usable_area_width_pt, usable_area_height_pt, margin_pt,
-                       transparent_background = True):
-    """
-    Render page number
-    """
-    ctx.save()
-    x_offset = 0
-    if page_number % 2:
-        x_offset += commons.convert_pt_to_dots(usable_area_width_pt)\
-                  - commons.convert_pt_to_dots(margin_pt)
-    y_offset = commons.convert_pt_to_dots(usable_area_height_pt)\
-             - commons.convert_pt_to_dots(margin_pt)
-    ctx.translate(x_offset, y_offset)
-
-    if transparent_background:
-        ctx.set_source_rgba(1, 1, 1, 0.6)
-    else:
-        ctx.set_source_rgba(0.8, 0.8, 0.8, 0.6)
-    ctx.rectangle(0, 0, commons.convert_pt_to_dots(margin_pt),
-                  commons.convert_pt_to_dots(margin_pt))
-    ctx.fill()
-
-    ctx.set_source_rgba(0, 0, 0, 1)
-    x_offset = commons.convert_pt_to_dots(margin_pt)/2
-    y_offset = commons.convert_pt_to_dots(margin_pt)/2
-    ctx.translate(x_offset, y_offset)
-    draw_simpletext_center(ctx, unicode(page_number), 0, 0)
-    ctx.restore()
-
diff --git a/ocitysmap2/i18n.py b/ocitysmap2/i18n.py
deleted file mode 100644
index d476717..0000000
--- a/ocitysmap2/i18n.py
+++ /dev/null
@@ -1,977 +0,0 @@
-# -*- coding: utf-8; mode: Python -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import re
-import gettext
-
-def _install_language(language, locale_path):
-    t = gettext.translation(domain='ocitysmap',
-                            localedir=locale_path,
-                            languages=[language],
-                            fallback=True)
-    t.install(unicode=True)
-
-class i18n:
-    """Functions needed to be implemented for a new language.
-       See i18n_fr_FR_UTF8 below for an example. """
-    def language_code(self):
-        pass
-
-    def user_readable_street(self, name):
-        pass
-
-    def first_letter_equal(self, a, b):
-        pass
-
-    def isrtl(self):
-        return False
-
-    def upper_unaccent_string(self, s):
-        return s.upper()
-
-class i18n_template_code_CODE(i18n):
-    def __init__(self, language, locale_path):
-        """Install the _() function for the chosen locale other
-           object initialisation"""
-
-        # It's important to convert to str() here because the map_language
-        # value coming from the database is Unicode, but setlocale() needs a
-        # non-unicode string as the locale name, otherwise it thinks it's a
-        # locale tuple.
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def language_code(self):
-        """returns the language code of the specific language
-           supported, e.g. fr_FR.UTF-8"""
-        return self.language
-
-    def user_readable_street(self, name):
-        """ transforms a street name into a suitable form for
-            the map index, e.g. Paris (Rue de) for French"""
-        return name
-
-    def first_letter_equal(self, a, b):
-        """returns True if the letters a and b are equal in the map index,
-           e.g. É and E are equals in French map index"""
-        return a == b
-
-    def isrtl(self):
-        return False
-
-
-class i18n_fr_generic(i18n):
-    APPELLATIONS = [ u"Accès", u"Allée", u"Allées", u"Autoroute", u"Avenue",
-                     u"Avenues", u"Barrage",
-                     u"Boulevard", u"Carrefour", u"Chaussée", u"Chemin",
-                     u"Chemin rural",
-                     u"Cheminement", u"Cale", u"Cales", u"Cavée", u"Cité",
-                     u"Clos", u"Coin", u"Côte", u"Cour", u"Cours", u"Descente",
-                     u"Degré", u"Escalier",
-                     u"Escaliers", u"Esplanade", u"Funiculaire",
-                     u"Giratoire", u"Hameau", u"Impasse", u"Jardin",
-                     u"Jardins", u"Liaison", u"Lotissement", u"Mail",
-                     u"Montée", u"Môle",
-                     u"Parc", u"Passage", u"Passerelle", u"Passerelles",
-                     u"Place", u"Placette", u"Pont", u"Promenade",
-                     u"Petite Avenue", u"Petite Rue", u"Quai",
-                     u"Rampe", u"Rang", u"Résidence", u"Rond-Point",
-                     u"Route forestière", u"Route", u"Rue", u"Ruelle",
-                     u"Square", u"Sente", u"Sentier", u"Sentiers", 
u"Terre-Plein",
-                     u"Télécabine", u"Traboule", u"Traverse", u"Tunnel",
-                     u"Venelle", u"Villa", u"Virage"
-                   ]
-    DETERMINANTS = [ u" des", u" du", u" de la", u" de l'",
-                     u" de", u" d'", u" aux", u""
-                   ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäãæ]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõœ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-    Y_ACCENT = re.compile(ur"[ÿ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        s = self.Y_ACCENT.sub("y", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_it_generic(i18n):
-    APPELLATIONS = [ u"Via", u"Viale", u"Piazza", u"Scali", u"Strada", 
u"Largo",
-                     u"Corso", u"Viale", u"Calle", u"Sottoportico",
-                    u"Sottoportego", u"Vicolo", u"Piazzetta" ]
-    DETERMINANTS = [ u" delle", u" dell'", u" dei", u" degli",
-                     u" della", u" del", u" di", u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_es_generic(i18n):
-    APPELLATIONS = [ u"Avenida", u"Avinguda", u"Calle", u"Callejón",
-            u"Calzada", u"Camino", u"Camí", u"Carrer", u"Carretera",
-            u"Glorieta", u"Parque", u"Pasaje", u"Pasarela", u"Paseo", u"Plaza",
-            u"Plaça", u"Privada", u"Puente", u"Ronda", u"Salida", u"Travesia" ]
-    DETERMINANTS = [ u" de la", u" de los", u" de las",
-                     u" dels", u" del", u" d'", u" de l'",
-                     u" de", u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        s = self.N_ACCENT.sub("n", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_ca_generic(i18n):
-
-    APPELLATIONS = [ # Catalan
-                     u"Autopista", u"Autovia", u"Avinguda",
-                     u"Baixada", u"Barranc", u"Barri", u"Barriada",
-                     u"Biblioteca", u"Carrer", u"Carreró", u"Carretera",
-                     u"Cantonada", u"Església", u"Estació", u"Hospital",
-                     u"Monestir", u"Monument", u"Museu", u"Passatge",
-                     u"Passeig", u"Plaça", u"Planta", u"Polígon",
-                     u"Pujada", u"Rambla", u"Ronda", u"Travessera",
-                     u"Travessia", u"Urbanització", u"Via",
-                     u"Avenida", u"Calle", u"Camino", u"Plaza",
-
-                     # Spanish (being distinct from Catalan)
-                     u"Acceso", u"Acequia", u"Alameda", u"Alquería",
-                     u"Andador", u"Angosta", u"Apartamentos", u"Apeadero",
-                     u"Arboleda", u"Arrabal", u"Arroyo", u"Autovía",
-                     u"Avenida", u"Bajada", u"Balneario", u"Banda",
-                     u"Barranco", u"Barranquil", u"Barrio", u"Bloque",
-                     u"Brazal", u"Bulevar", u"Calle", u"Calleja",
-                     u"Callejón", u"Callejuela", u"Callizo", u"Calzada",
-                     u"Camino", u"Camping", u"Cantera", u"Cantina",
-                     u"Cantón", u"Carrera", u"Carrero", u"Carreterín",
-                     u"Carretil", u"Carril", u"Caserío", u"Chalet",
-                     u"Cinturón", u"Circunvalación", u"Cobertizo",
-                     u"Colonia", u"Complejo", u"Conjunto", u"Convento",
-                     u"Cooperativa", u"Corral", u"Corralillo", u"Corredor",
-                     u"Cortijo", u"Costanilla", u"Costera", u"Cuadra",
-                     u"Cuesta", u"Dehesa", u"Demarcación", u"Diagonal",
-                     u"Diseminado", u"Edificio", u"Empresa", u"Entrada",
-                     u"Escalera", u"Escalinata", u"Espalda", u"Estación",
-                     u"Estrada", u"Explanada", u"Extramuros", u"Extrarradio",
-                     u"Fábrica", u"Galería", u"Glorieta", u"Gran Vía",
-                     u"Granja", u"Hipódromo", u"Jardín", u"Ladera",
-                     u"Llanura", u"Malecón", u"Mercado", u"Mirador",
-                     u"Monasterio", u"Muelle", u"Núcleo", u"Palacio",
-                     u"Pantano", u"Paraje", u"Parque", u"Particular",
-                     u"Partida", u"Pasadizo", u"Pasaje", u"Paseo",
-                     u"Paseo marítimo", u"Pasillo", u"Plaza", u"Plazoleta",
-                     u"Plazuela", u"Poblado", u"Polígono", u"Polígono 
industrial",
-                     u"Portal", u"Pórtico", u"Portillo", u"Prazuela",
-                     u"Prolongación", u"Pueblo", u"Puente", u"Puerta",
-                     u"Puerto", u"Punto kilométrico", u"Rampla",
-                     u"Residencial", u"Ribera", u"Rincón", u"Rinconada",
-                     u"Sanatorio", u"Santuario", u"Sector", u"Sendera",
-                     u"Sendero", u"Subida", u"Torrente", u"Tránsito",
-                     u"Transversal", u"Trasera", u"Travesía", u"Urbanización",
-                     u"Vecindario", u"Vereda", u"Viaducto", u"Viviendas",
-
-                     # French (being distinct from Catalan and Spanish)
-                     u"Accès", u"Allée", u"Allées", u"Autoroute", u"Avenue", 
u"Barrage",
-                     u"Boulevard", u"Carrefour", u"Chaussée", u"Chemin",
-                     u"Cheminement", u"Cale", u"Cales", u"Cavée", u"Cité",
-                     u"Clos", u"Coin", u"Côte", u"Cour", u"Cours", u"Descente",
-                     u"Degré", u"Escalier",
-                     u"Escaliers", u"Esplanade", u"Funiculaire",
-                     u"Giratoire", u"Hameau", u"Impasse", u"Jardin",
-                     u"Jardins", u"Liaison", u"Mail", u"Montée", u"Môle",
-                     u"Parc", u"Passage", u"Passerelle", u"Passerelles",
-                     u"Place", u"Placette", u"Pont", u"Promenade",
-                     u"Petite Avenue", u"Petite Rue", u"Quai",
-                     u"Rampe", u"Rang", u"Résidence", u"Rond-Point",
-                     u"Route forestière", u"Route", u"Rue", u"Ruelle",
-                     u"Square", u"Sente", u"Sentier", u"Sentiers", 
u"Terre-Plein",
-                     u"Télécabine", u"Traboule", u"Traverse", u"Tunnel",
-                     u"Venelle", u"Villa", u"Virage"
-                   ]
-
-    DETERMINANTS = [ # Catalan
-                     u" de", u" de la", u" del", u" dels", u" d'",
-                     u" de l'", u" de sa", u" de son", u" de s'",
-                     u" de ses", u" d'en", u" de na", u" de n'",
-
-                     # Spanish (being distinct from Catalan)
-                     u" de las",  u" de los",
-
-                     # French (being distinct from Catalan and Spanish)
-                     u" du",
-                     u""]
-
-
-    DETERMINANTS = [ u" de", u" de la", u" del", u" de las",
-                     u" dels", u" de los", u" d'", u" de l'", u"de sa", u"de 
son", u"de s'",
-                     u"de ses", u"d'en", u"de na", u"de n'", u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
-    C_ACCENT = re.compile(ur"[ç]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        s = self.N_ACCENT.sub("n", s)
-        s = self.C_ACCENT.sub("c", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_pt_br_generic(i18n):
-    APPELLATIONS = [ u"Aeroporto", u"Aer.", u"Alameda", u"Al.", 
u"Apartamento", u"Ap.", 
-                     u"Área", u"Avenida", u"Av.", u"Beco", u"Bc.", u"Bloco", 
u"Bl.", 
-                     u"Caminho", u"Cam.", u"Campo", u"Chácara", u"Colônia",
-                     u"Condomínio", u"Conjunto", u"Cj.", u"Distrito", 
u"Esplanada", u"Espl.", 
-                     u"Estação", u"Est.", u"Estrada", u"Estr.", u"Favela", 
u"Fazenda",
-                     u"Feira", u"Jardim", u"Jd.", u"Ladeira", u"Lago",
-                     u"Lagoa", u"Largo", u"Loteamento", u"Morro", u"Núcleo",
-                     u"Parque", u"Pq.", u"Passarela", u"Pátio", u"Praça", 
u"Pç.", u"Quadra",
-                     u"Recanto", u"Residencial", u"Resid.", u"Rua", u"R.", 
-                     u"Setor", u"Sítio", u"Travessa", u"Tv.", u"Trecho", 
u"Trevo",
-                     u"Vale", u"Vereda", u"Via", u"V.", u"Viaduto", u"Viela",
-                     u"Vila", u"Vl." ]
-    DETERMINANTS = [ u" do", u" da", u" dos", u" das", u"" ]
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_ar_generic(i18n):
-    APPELLATIONS = [ u"شارع", u"طريق", u"زقاق", u"نهج", u"جادة",
-                     u"ممر", u"حارة",
-                     u"كوبري", u"كوبرى", u"جسر", u"مطلع", u"منزل",
-                     u"مفرق", u"ملف", u"تقاطع",
-                     u"ساحل",
-                     u"ميدان", u"ساحة", u"دوار" ]
-
-    DETERMINANTS = [ u" ال", u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    A_ACCENT = re.compile(ur"[اإآ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.A_ACCENT.sub("أ", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-    def isrtl(self):
-        return True
-
-class i18n_ru_generic(i18n):
-    APPELLATIONS = [ u"ул", u"бул", u"пер", u"пр", u"улица", u"бульвар", 
u"проезд",
-                     u"проспект", u"площадь", u"сквер", u"парк" ]
-    # only "ул." and "пер." are recommended shortenings, however other words 
can 
-    # occur shortened.
-    #
-    # http://bit.ly/6ASISp (OSM wiki)
-    #
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)\.?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        # usually, there are no accents in russian names, only "ё" sometimes, 
but
-        # not as first letter
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_nl_generic(i18n):
-    #
-    # Dutch streets are often named after people and include a title.
-    # The title will be captured as part of the <prefix>
-    #
-    APPELLATIONS = [ u"St.", u"Sint", u"Ptr.", u"Pater",
-                     u"Prof.", u"Professor", u"Past.", u"Pastoor",
-                     u"Pr.", u"Prins", u"Prinses", u"Gen.", u"Generaal",
-                     u"Mgr.", u"Monseigneur", u"Mr.", u"Meester",
-                     u"Burg.", u"Burgermeester", u"Dr.", u"Dokter",
-                     u"Ir.", u"Ingenieur", u"Ds.", u"Dominee", u"Deken",
-                     u"Drs.", u"Maj.", u"Majoor",
-                     # counting words before street name,
-                     # e.g. "1e Walstraat" => "Walstraat (1e)"
-                     u"\d+e",
-                     u"" ]
-    #
-    # Surnames in Dutch streets named after people tend to have the middle name
-    # listed after the rest of the surname,
-    # e.g. "Prins van Oranjestraat" => "Oranjestraat (Prins van)"
-    # Likewise, articles are captured as part of the prefix,
-    # e.g. "Den Urling" => "Urling (Den)"
-    #
-    DETERMINANTS = [ u"\s?van der", u"\s?van den", u"\s?van de", u"\s?van",
-                     u"\s?Den", u"\s?D'n", u"\s?D'", u"\s?De", u"\s?'T", 
u"\s?Het",
-                     u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)),
-                                      re.IGNORECASE | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        #
-        # Make sure name actually contains something,
-        # the PREFIX_REGEXP.match fails on zero-length strings
-        #
-        if len(name) == 0:
-            return name
-
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        matches = self.PREFIX_REGEXP.match(name)
-        #
-        # If no prefix was captured, that's okay. Don't substitute
-        # the name however, "<name> ()" looks silly
-        #
-        if matches == None:
-            return name
-
-        if matches.group('prefix'):
-            name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_hr_HR(i18n):
-    # for upper_unaccent_string
-    C_ACCENT = re.compile(ur"[ćč]", re.IGNORECASE | re.UNICODE)
-    D_ACCENT = re.compile(ur"đ|dž", re.IGNORECASE | re.UNICODE)
-    N_ACCENT = re.compile(ur"nj", re.IGNORECASE | re.UNICODE)
-    L_ACCENT = re.compile(ur"lj", re.IGNORECASE | re.UNICODE)
-    S_ACCENT = re.compile(ur"š", re.IGNORECASE | re.UNICODE)
-    Z_ACCENT = re.compile(ur"ž", re.IGNORECASE | re.UNICODE)
-
-    def upper_unaccent_string(self, s):
-        s = self.C_ACCENT.sub("c", s)
-        s = self.D_ACCENT.sub("d", s)
-        s = self.N_ACCENT.sub("n", s)
-        s = self.L_ACCENT.sub("l", s)
-        s = self.S_ACCENT.sub("s", s)
-        s = self.Z_ACCENT.sub("z", s)
-        return s.upper()
-
-    def __init__(self, language, locale_path):
-        """Install the _() function for the chosen locale other
-           object initialisation"""
-        self.language = str(language) # FIXME: why do we have unicode here?
-        _install_language(language, locale_path)
-
-    def language_code(self):
-        """returns the language code of the specific language
-           supported, e.g. fr_FR.UTF-8"""
-        return self.language
-
-    def user_readable_street(self, name):
-        """ transforms a street name into a suitable form for
-            the map index, e.g. Paris (Rue de) for French"""
-        return name
-
-    ## FIXME: only first letter does not work for Croatian digraphs (dž, lj, 
nj)
-    def first_letter_equal(self, a, b):
-        """returns True if the letters a and b are equal in the map index,
-           e.g. É and E are equals in French map index"""
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_pl_generic(i18n):
-
-    APPELLATIONS = [ u"Dr.", u"Doktora", u"Ks.", u"Księdza",
-                     u"Generała", u"Gen.",
-                     u"Aleja", u"Plac", u"Pl.",
-                     u"Rondo", u"rondo", u"Profesora",
-                     u"Prof.",
-                     u"" ]
-
-    DETERMINANTS = [ u"\s?im.", u"\s?imienia", u"\s?pw.",
-                     u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)),
-                                      re.IGNORECASE | re.UNICODE)
-
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        #
-        # Make sure name actually contains something,
-        # the PREFIX_REGEXP.match fails on zero-length strings
-        #
-        if len(name) == 0:
-            return name
-
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        matches = self.PREFIX_REGEXP.match(name)
-        #
-        # If no prefix was captured, that's okay. Don't substitute
-        # the name however, "<name> ()" looks silly
-        #
-        if matches == None:
-            return name
-
-        if matches.group('prefix'):
-            name = self.PREFIX_REGEXP.sub(r"\g<name>, \g<prefix>", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return a == b
-
-class i18n_tr_TR_generic(i18n):
-    APPELLATIONS = [ u"Sokak", u"Sokağı" ]
-    DETERMINANTS = []
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        #
-        # Make sure name actually contains something,
-        # the PREFIX_REGEXP.match fails on zero-length strings
-        #
-        if len(name) == 0:
-            return name
-
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_de_generic(i18n):
-    #
-    # German streets are often named after people and include a title.
-    # The title will be captured as part of the <prefix>
-       # Covering airport names and "New"/"Old" as prefixes as well
-    #
-    APPELLATIONS = [ u"Alte", u"Alter", u"Doktor", u"Dr.",
-                     u"Flughafen", u"Flugplatz", u"Gen.,", u"General",
-                     u"Neue", u"Neuer", u"Platz",
-                     u"Prinz", u"Prinzessin", u"Prof.",
-                     u"Professor" ]
-    #
-    # Surnames in german streets named after people tend to have the middle 
name
-    # listed after the rest of the surname,
-    # e.g. "Platz der deutschen Einheit" => "deutschen Einheit (Platz der)"
-    # Likewise, articles are captured as part of the prefix,
-    # e.g. "An der Märchenwiese" => "Märchenwiese (An der)"
-    #
-    DETERMINANTS = [ u"\s?An den", u"\s?An der", u"\s?Am",
-                     u"\s?Auf den" , u"\s?Auf der"
-                     u" an", u" des", u" der", u" von", u" vor"]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        #
-        # Make sure name actually contains something,
-        # the PREFIX_REGEXP.match fails on zero-length strings
-        #
-        if len(name) == 0:
-            return name
-
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_ast_generic(i18n):
-
-    APPELLATIONS = [ # Asturian
-                     u"Accesu", u"Autopista", u"Autovia", u"Avenida",
-                     u"Baxada", u"Barrancu", u"Barriu", u"Barriada",
-                     u"Biblioteca", u"Cai", u"Caleya",
-                     u"Calzada", u"Camín", u"Carretera", u"Cuesta",
-                     u"Estación", u"Hospital", u"Iglesia", u"Monasteriu",
-                     u"Monumentu", u"Muelle", u"Muséu",
-                     u"Palaciu", u"Parque", u"Pasadizu", u"Pasaxe",
-                     u"Paséu", u"Planta", u"Plaza", u"Polígonu",
-                     u"Ronda", u"Travesía", u"Urbanización", u"Via",
-                     u"Xardín", u"Xardinos",
-
-                     # Spanish (different from Asturian)
-                     u"Acceso", u"Acequia", u"Alameda", u"Alquería",
-                     u"Andador", u"Angosta", u"Apartamentos", u"Apeadero",
-                     u"Arboleda", u"Arrabal", u"Arroyo", u"Autovía",
-                     u"Bajada", u"Balneario", u"Banda",
-                     u"Barranco", u"Barranquil", u"Barrio", u"Bloque",
-                     u"Brazal", u"Bulevar", u"Calle", u"Calleja",
-                     u"Callejón", u"Callejuela", u"Callizo",
-                     u"Camino", u"Camping", u"Cantera", u"Cantina",
-                     u"Cantón", u"Carrera", u"Carrero", u"Carreterín",
-                     u"Carretil", u"Carril", u"Caserío", u"Chalet",
-                     u"Cinturón", u"Circunvalación", u"Cobertizo",
-                     u"Colonia", u"Complejo", u"Conjunto", u"Convento",
-                     u"Cooperativa", u"Corral", u"Corralillo", u"Corredor",
-                     u"Cortijo", u"Costanilla", u"Costera", u"Cuadra",
-                     u"Dehesa", u"Demarcación", u"Diagonal",
-                     u"Diseminado", u"Edificio", u"Empresa", u"Entrada",
-                     u"Escalera", u"Escalinata", u"Espalda", u"Estación",
-                     u"Estrada", u"Explanada", u"Extramuros", u"Extrarradio",
-                     u"Fábrica", u"Galería", u"Glorieta", u"Gran Vía",
-                     u"Granja", u"Hipódromo", u"Jardín", u"Ladera",
-                     u"Llanura", u"Malecón", u"Mercado", u"Mirador",
-                     u"Monasterio", u"Núcleo", u"Palacio",
-                     u"Pantano", u"Paraje", u"Particular",
-                     u"Partida", u"Pasadizo", u"Pasaje", u"Paseo",
-                     u"Paseo marítimo", u"Pasillo", u"Plaza", u"Plazoleta",
-                     u"Plazuela", u"Poblado", u"Polígono", u"Polígono 
industrial",
-                     u"Portal", u"Pórtico", u"Portillo", u"Prazuela",
-                     u"Prolongación", u"Pueblo", u"Puente", u"Puerta",
-                     u"Puerto", u"Punto kilométrico", u"Rampla",
-                     u"Residencial", u"Ribera", u"Rincón", u"Rinconada",
-                     u"Sanatorio", u"Santuario", u"Sector", u"Sendera",
-                     u"Sendero", u"Subida", u"Torrente", u"Tránsito",
-                     u"Transversal", u"Trasera", u"Travesía", u"Urbanización",
-                     u"Vecindario", u"Vereda", u"Viaducto", u"Viviendas",
-                   ]
-
-    DETERMINANTS = [ # Asturian
-                     u" de", u" de la", u" del", u" de les", u" d'",
-                     u" de los", u" de l'",
-
-                     # Spanish (different from Asturian)
-                     u" de las",
-                     u""]
-
-
-    DETERMINANTS = [ u" de", u" de la", u" del", u" de les",
-                     u" de los", u" de las", u" d'", u" de l'", u"" ]
-
-    SPACE_REDUCE = re.compile(r"\s+")
-    PREFIX_REGEXP = re.compile(r"^(?P<prefix>(%s)(%s)?)\s?\b(?P<name>.+)" %
-                                    ("|".join(APPELLATIONS),
-                                     "|".join(DETERMINANTS)), re.IGNORECASE
-                                                                 | re.UNICODE)
-
-    # for IndexPageGenerator.upper_unaccent_string
-    E_ACCENT = re.compile(ur"[éèêëẽ]", re.IGNORECASE | re.UNICODE)
-    I_ACCENT = re.compile(ur"[íìîïĩ]", re.IGNORECASE | re.UNICODE)
-    A_ACCENT = re.compile(ur"[áàâäã]", re.IGNORECASE | re.UNICODE)
-    O_ACCENT = re.compile(ur"[óòôöõ]", re.IGNORECASE | re.UNICODE)
-    U_ACCENT = re.compile(ur"[úùûüũ]", re.IGNORECASE | re.UNICODE)
-    N_ACCENT = re.compile(ur"[ñ]", re.IGNORECASE | re.UNICODE)
-    H_ACCENT = re.compile(ur"[ḥ]", re.IGNORECASE | re.UNICODE)
-    L_ACCENT = re.compile(ur"[ḷ]", re.IGNORECASE | re.UNICODE)
-
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def upper_unaccent_string(self, s):
-        s = self.E_ACCENT.sub("e", s)
-        s = self.I_ACCENT.sub("i", s)
-        s = self.A_ACCENT.sub("a", s)
-        s = self.O_ACCENT.sub("o", s)
-        s = self.U_ACCENT.sub("u", s)
-        s = self.N_ACCENT.sub("n", s)
-        s = self.H_ACCENT.sub("h", s)
-        s = self.L_ACCENT.sub("l", s)
-        return s.upper()
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        name = name.strip()
-        name = self.SPACE_REDUCE.sub(" ", name)
-        name = self.PREFIX_REGEXP.sub(r"\g<name> (\g<prefix>)", name)
-        return name
-
-    def first_letter_equal(self, a, b):
-        return self.upper_unaccent_string(a) == self.upper_unaccent_string(b)
-
-class i18n_generic(i18n):
-    def __init__(self, language, locale_path):
-        self.language = str(language)
-        _install_language(language, locale_path)
-
-    def language_code(self):
-        return self.language
-
-    def user_readable_street(self, name):
-        return name
-
-    def first_letter_equal(self, a, b):
-        return a == b
-
-# When not listed in the following map, default language class will be
-# i18n_generic
-language_class_map = {
-    'fr_BE.UTF-8': i18n_fr_generic,
-    'fr_FR.UTF-8': i18n_fr_generic,
-    'fr_CA.UTF-8': i18n_fr_generic,
-    'fr_CH.UTF-8': i18n_fr_generic,
-    'fr_LU.UTF-8': i18n_fr_generic,
-    'en_AG': i18n_generic,
-    'en_AU.UTF-8': i18n_generic,
-    'en_BW.UTF-8': i18n_generic,
-    'en_CA.UTF-8': i18n_generic,
-    'en_DK.UTF-8': i18n_generic,
-    'en_GB.UTF-8': i18n_generic,
-    'en_HK.UTF-8': i18n_generic,
-    'en_IE.UTF-8': i18n_generic,
-    'en_IN': i18n_generic,
-    'en_NG': i18n_generic,
-    'en_NZ.UTF-8': i18n_generic,
-    'en_PH.UTF-8': i18n_generic,
-    'en_SG.UTF-8': i18n_generic,
-    'en_US.UTF-8': i18n_generic,
-    'en_ZA.UTF-8': i18n_generic,
-    'en_ZW.UTF-8': i18n_generic,
-    'nl_BE.UTF-8': i18n_nl_generic,
-    'nl_NL.UTF-8': i18n_nl_generic,
-    'it_IT.UTF-8': i18n_it_generic,
-    'it_CH.UTF-8': i18n_it_generic,
-    'de_AT.UTF-8': i18n_de_generic,
-    'de_BE.UTF-8': i18n_de_generic,
-    'de_DE.UTF-8': i18n_de_generic,
-    'de_LU.UTF-8': i18n_de_generic,
-    'de_CH.UTF-8': i18n_de_generic,
-    'es_ES.UTF-8': i18n_es_generic,
-    'es_AR.UTF-8': i18n_es_generic,
-    'es_BO.UTF-8': i18n_es_generic,
-    'es_CL.UTF-8': i18n_es_generic,
-    'es_CR.UTF-8': i18n_es_generic,
-    'es_DO.UTF-8': i18n_es_generic,
-    'es_EC.UTF-8': i18n_es_generic,
-    'es_SV.UTF-8': i18n_es_generic,
-    'es_GT.UTF-8': i18n_es_generic,
-    'es_HN.UTF-8': i18n_es_generic,
-    'es_MX.UTF-8': i18n_es_generic,
-    'es_NI.UTF-8': i18n_es_generic,
-    'es_PA.UTF-8': i18n_es_generic,
-    'es_PY.UTF-8': i18n_es_generic,
-    'es_PE.UTF-8': i18n_es_generic,
-    'es_PR.UTF-8': i18n_es_generic,
-    'es_US.UTF-8': i18n_es_generic,
-    'es_UY.UTF-8': i18n_es_generic,
-    'es_VE.UTF-8': i18n_es_generic,
-    'ca_ES.UTF-8': i18n_ca_generic,
-    'ca_AD.UTF-8': i18n_ca_generic,
-    'ca_FR.UTF-8': i18n_ca_generic,
-    'pt_BR.UTF-8': i18n_pt_br_generic,
-    'da_DK.UTF-8': i18n_generic,
-    'ar_AE.UTF-8': i18n_ar_generic,
-    'ar_BH.UTF-8': i18n_ar_generic,
-    'ar_DZ.UTF-8': i18n_ar_generic,
-    'ar_EG.UTF-8': i18n_ar_generic,
-    'ar_IN': i18n_ar_generic,
-    'ar_IQ.UTF-8': i18n_ar_generic,
-    'ar_JO.UTF-8': i18n_ar_generic,
-    'ar_KW.UTF-8': i18n_ar_generic,
-    'ar_LB.UTF-8': i18n_ar_generic,
-    'ar_LY.UTF-8': i18n_ar_generic,
-    'ar_MA.UTF-8': i18n_ar_generic,
-    'ar_OM.UTF-8': i18n_ar_generic,
-    'ar_QA.UTF-8': i18n_ar_generic,
-    'ar_SA.UTF-8': i18n_ar_generic,
-    'ar_SD.UTF-8': i18n_ar_generic,
-    'ar_SY.UTF-8': i18n_ar_generic,
-    'ar_TN.UTF-8': i18n_ar_generic,
-    'ar_YE.UTF-8': i18n_ar_generic,
-    'hr_HR.UTF-8': i18n_hr_HR,
-    'ru_RU.UTF-8': i18n_ru_generic,
-    'pl_PL.UTF-8': i18n_pl_generic,
-    'nb_NO.UTF-8': i18n_generic,
-    'nn_NO.UTF-8': i18n_generic,
-    'tr_TR.UTF-8': i18n_tr_TR_generic,
-    'ast_ES.UTF-8': i18n_ast_generic,
-    'sk_SK.UTF-8': i18n_generic,
-}
-
-def install_translation(locale_name, locale_path):
-    """Return a new i18n class instance, depending on the specified
-    locale name (eg. "fr_FR.UTF-8"). See output of "locale -a" for a
-    list of system-supported locale names. When none matching, default
-    class is i18n_generic"""
-    language_class = language_class_map.get(locale_name, i18n_generic)
-    return language_class(locale_name, locale_path)
diff --git a/ocitysmap2/indexlib/__init__.py b/ocitysmap2/indexlib/__init__.py
deleted file mode 100644
index 23a8bc6..0000000
--- a/ocitysmap2/indexlib/__init__.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-if __name__ == '__main__':
-    import os
-    import string
-    import random
-    import psycopg2
-    import cairo
-    import logging
-
-    logging.basicConfig(level=logging.DEBUG)
-
-    from ocitysmap2 import i18n, coords
-    from ocitysmap2.maplib.grid import Grid
-
-    from indexer  import StreetIndex
-    from renderer import StreetIndexRenderer
-    from commons  import IndexCategory, IndexItem
-
-    random.seed(42)
-
-    lang = "fr_FR.UTF-8"
-    #lang = "ar_MA.UTF-8"
-    #lang = "zh_CN.utf8"
-    i18n = i18n.install_translation(lang,
-                                    os.path.join(os.path.dirname(__file__),
-                                                 "..", "..", "locale"))
-
-    bbox = coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699) # France
-    #bbox = coords.BoundingBox(34.0322, -6.8648, 34.0073, -6.8133) # Moroco
-    #bbox = bbox = coords.BoundingBox(22.5786, 114.0308, 22.5231, 114.1338) # 
CN
-
-    # Build the list of index items
-    db = psycopg2.connect(user='maposmatic',
-                          password='waeleephoo3Aew3u',
-                          host='localhost',
-                          database='maposmatic')
-
-    street_index = StreetIndex(db, bbox.as_wkt(), i18n)
-    print street_index.categories
-
-    # Render the items
-    class i18nMock:
-        def __init__(self, rtl):
-            self.rtl = rtl
-        def isrtl(self):
-            return self.rtl
-
-    width  = 2.5*(20 / 2.54) * 72
-    height = 2.5*(29 / 2.54) * 72
-
-    surface = cairo.PDFSurface('/tmp/myindex.pdf', width, height)
-
-    # Map index to grid
-    grid = Grid(bbox, rtl = False)
-    street_index.apply_grid(grid)
-
-    index = StreetIndexRenderer(i18nMock(False), street_index.categories)
-
-    def _render(freedom_dimension, alignment):
-        x,y,w,h = 50, 50, width-100, height-100
-
-        # Draw constraining rectangle
-        ctx = cairo.Context(surface)
-
-        ctx.save()
-        ctx.set_source_rgb(.2,0,0)
-        ctx.rectangle(x,y,w,h)
-        ctx.stroke()
-
-        # Precompute index area
-        rendering_area = index.precompute_occupation_area(surface, x,y,w,h,
-                                                          freedom_dimension,
-                                                          alignment)
-
-        # Draw a green background for the precomputed area
-        ctx.set_source_rgba(0,1,0,.5)
-        ctx.rectangle(rendering_area.x, rendering_area.y,
-                      rendering_area.w, rendering_area.h)
-        ctx.fill()
-        ctx.restore()
-
-        # Render the index
-        index.render(ctx, rendering_area)
-
-    _render('height', 'top')
-    surface.show_page()
-    _render('height', 'bottom')
-    surface.show_page()
-    _render('width', 'left')
-    surface.show_page()
-    _render('width', 'right')
-    surface.show_page()
-
-    ##
-    ## Now demo with RTL = True
-    ##
-
-    # Map index to grid
-    grid = Grid(bbox, rtl = True)
-    street_index.apply_grid(grid)
-
-    index = StreetIndexRenderer(i18nMock(True), street_index.categories)
-    _render('height', 'top')
-    surface.show_page()
-    _render('height', 'bottom')
-    surface.show_page()
-    _render('width', 'left')
-    surface.show_page()
-    _render('width', 'right')
-
-    surface.finish()
-    print "Generated /tmp/myindex.pdf."
diff --git a/ocitysmap2/indexlib/commons.py b/ocitysmap2/indexlib/commons.py
deleted file mode 100644
index 6942577..0000000
--- a/ocitysmap2/indexlib/commons.py
+++ /dev/null
@@ -1,282 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import pango
-
-import os, sys
-sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
-import draw_utils
-
-class IndexEmptyError(Exception):
-    """This exception is raised when no data is to be rendered in the index."""
-    pass
-
-class IndexDoesNotFitError(Exception):
-    """This exception is raised when the index does not fit in the given
-    graphical area, even after trying smaller font sizes."""
-    pass
-
-NUMBER_CATEGORY_NAME = '0-9'
-
-class IndexCategory:
-    """
-    The IndexCategory represents a set of index items that belong to the same
-    category (their first letter is the same or they are of the same amenity
-    type).
-    """
-    name = None
-    items = None
-    is_street = False
-
-    def __init__(self, name, items=None, is_street=True):
-        assert name is not None
-        self.name = name
-        self.items = items or list()
-        self.is_street = is_street
-
-    def __str__(self):
-        return '<%s (%s)>' % (self.name, map(str, self.items))
-
-    def __repr__(self):
-        return 'IndexCategory(%s, %s)' % (repr(self.name),
-                                          repr(self.items))
-
-    def draw(self, rtl, ctx, pc, layout, fascent, fheight,
-             baseline_x, baseline_y):
-        """Draw this category header.
-
-        Args:
-            rtl (boolean): whether to draw right-to-left or not.
-            ctx (cairo.Context): the Cairo context to draw to.
-            pc (pangocairo.CairoContext): the PangoCairo context.
-            layout (pango.layout): the Pango layout to draw text into.
-            fascent (int): font ascent.
-            fheight (int): font height.
-            baseline_x (int): base X axis position.
-            baseline_y (int): base Y axis position.
-        """
-
-        ctx.save()
-        ctx.set_source_rgb(0.9, 0.9, 0.9)
-        ctx.rectangle(baseline_x, baseline_y - fascent,
-                      layout.get_width() / pango.SCALE, fheight)
-        ctx.fill()
-
-        ctx.set_source_rgb(0.0, 0.0, 0.0)
-        draw_utils.draw_text_center(ctx, pc, layout, fascent, fheight,
-                                    baseline_x, baseline_y, self.name)
-        ctx.restore()
-
-    def get_all_item_labels(self):
-        return [x.label for x in self.items]
-
-    def get_all_item_squares(self):
-        return [x.squares for x in self.items]
-
-
-class IndexItem:
-    """
-    An IndexItem represents one item in the index (a street or a POI). It
-    contains the item label (street name, POI name or description) and the
-    humanized squares description.
-    """
-    __slots__    = ['label', 'endpoint1', 'endpoint2', 'location_str']
-    label        = None # str
-    endpoint1    = None # coords.Point
-    endpoint2    = None # coords.Point
-    location_str = None # str or None
-    page_number  = None # integer or None. Only used by multi-page renderer.
-
-    def __init__(self, label, endpoint1, endpoint2, page_number=None):
-        assert label is not None
-        self.label        = label
-        self.endpoint1    = endpoint1
-        self.endpoint2    = endpoint2
-        self.location_str = None
-        self.page_number  = page_number
-
-    def __str__(self):
-        return '%s...%s' % (self.label, self.location_str)
-
-    def __repr__(self):
-        return ('IndexItem(%s, %s, %s, %s, %s)'
-                % (repr(self.label), self.endpoint1, self.endpoint2,
-                   repr(self.location_str), repr(self.page_number)))
-
-    def label_drawing_width(self, layout):
-        layout.set_text(self.label)
-        return float(layout.get_size()[0]) / pango.SCALE
-
-    def label_drawing_height(self, layout):
-        layout.set_text(self.label)
-        return float(layout.get_size()[1]) / pango.SCALE
-
-    def location_drawing_width(self, layout):
-        layout.set_text(self.location_str)
-        return float(layout.get_size()[0]) / pango.SCALE
-
-    def draw(self, rtl, ctx, pc, column_layout, fascent, fheight,
-             baseline_x, baseline_y,
-             label_layout=None, label_height=0, location_width=0):
-        """Draw this index item to the provided Cairo context. It prints the
-        label, the squares definition and the dotted line, with respect to the
-        RTL setting.
-
-        Args:
-            rtl (boolean): right-to-left localization.
-            ctx (cairo.Context): the Cairo context to draw to.
-            pc (pangocairo.PangoCairo): the PangoCairo context for text
-                drawing.
-            column_layout (pango.Layout): the Pango layout to use for text
-                rendering, pre-configured with the appropriate font.
-            fascent (int): font ascent.
-            fheight (int): font height.
-            baseline_x (int): X axis coordinate of the baseline.
-            baseline_y (int): Y axis coordinate of the baseline.
-        Optional args (in case of label wrapping):
-            label_layout (pango.Layout): the Pango layout to use for text
-                rendering, in case the label should be wrapped
-            label_height (int): height of the big label
-            location_width (int): width of the 'location' part
-        """
-
-        # Fallbacks in case we dont't have a wrapping label
-        if label_layout == None:
-            label_layout = column_layout
-        if label_height == 0:
-            label_height = fheight
-
-        if not self.location_str:
-            location_str = '???'
-        else:
-            location_str = self.location_str
-
-        ctx.save()
-        if not rtl:
-            _, _, line_start = draw_utils.draw_text_left(ctx, pc, label_layout,
-                                                         fascent, fheight,
-                                                         baseline_x, 
baseline_y,
-                                                         self.label)
-            line_end, _, _ = draw_utils.draw_text_right(ctx, pc, column_layout,
-                                                        fascent, fheight,
-                                                        baseline_x, baseline_y,
-                                                        location_str)
-        else:
-            _, _, line_start = draw_utils.draw_text_left(ctx, pc, 
column_layout,
-                                                         fascent, fheight,
-                                                         baseline_x, 
baseline_y,
-                                                         location_str)
-            line_end, _, _ = draw_utils.draw_text_right(ctx, pc, label_layout,
-                                                        fascent, fheight,
-                                                        (baseline_x
-                                                         + location_width),
-                                                        baseline_y,
-                                                        self.label)
-
-        # In case of empty label, we don't draw the dots
-        if self.label != '':
-            draw_utils.draw_dotted_line(ctx, max(fheight/12, 1),
-                                        line_start + fheight/4, baseline_y,
-                                        line_end - line_start - fheight/2)
-        ctx.restore()
-
-    def update_location_str(self, grid):
-        """
-        Update the location_str field from the given Grid object.
-
-        Args:
-           grid (ocitysmap2.Grid): the Grid object from which we
-           compute the location strings
-
-        Returns:
-           Nothing, but the location_str field will have been altered
-        """
-        if self.endpoint1 is not None:
-            ep1_label = grid.get_location_str( * self.endpoint1.get_latlong())
-        else:
-            ep1_label = None
-        if self.endpoint2 is not None:
-            ep2_label = grid.get_location_str( * self.endpoint2.get_latlong())
-        else:
-            ep2_label = None
-        if ep1_label is None:
-            ep1_label = ep2_label
-        if ep2_label is None:
-            ep2_label = ep1_label
-
-        if ep1_label == ep2_label:
-            if ep1_label is None:
-                self.location_str = "???"
-            self.location_str = ep1_label
-        elif grid.rtl:
-            self.location_str = "%s-%s" % (max(ep1_label, ep2_label),
-                                           min(ep1_label, ep2_label))
-        else:
-            self.location_str = "%s-%s" % (min(ep1_label, ep2_label),
-                                           max(ep1_label, ep2_label))
-
-        if self.page_number is not None:
-            if grid.rtl:
-                self.location_str = "%s, %d" % (self.location_str,
-                                                self.page_number)
-            else:
-                self.location_str = "%d, %s" % (self.page_number,
-                                                self.location_str)
-
-if __name__ == "__main__":
-    import cairo
-    import pangocairo
-
-    surface = cairo.PDFSurface('/tmp/idx_commons.pdf', 1000, 1000)
-
-    ctx = cairo.Context(surface)
-    pc = pangocairo.CairoContext(ctx)
-
-    font_desc = pango.FontDescription('DejaVu')
-    font_desc.set_size(12 * pango.SCALE)
-
-    layout = pc.create_layout()
-    layout.set_font_description(font_desc)
-    layout.set_width(200 * pango.SCALE)
-
-    font = layout.get_context().load_font(font_desc)
-    font_metric = font.get_metrics()
-
-    fascent = font_metric.get_ascent() / pango.SCALE
-    fheight = ((font_metric.get_ascent() + font_metric.get_descent())
-               / pango.SCALE)
-
-    first_item  = IndexItem('First Item', None, None)
-    second_item = IndexItem('Second Item', None, None)
-    category    = IndexCategory('Hello world !', [first_item, second_item])
-
-    category.draw(False, ctx, pc, layout, fascent, fheight,
-                  72, 80)
-    first_item.draw(False, ctx, pc, layout, fascent, fheight,
-                    72, 100)
-    second_item.draw(False, ctx, pc, layout, fascent, fheight,
-                     72, 120)
-
-    surface.finish()
-    print "Generated /tmp/idx_commons.pdf"
diff --git a/ocitysmap2/indexlib/indexer.py b/ocitysmap2/indexlib/indexer.py
deleted file mode 100644
index 22a53eb..0000000
--- a/ocitysmap2/indexlib/indexer.py
+++ /dev/null
@@ -1,488 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-import logging
-import locale
-import psycopg2
-import csv
-import datetime
-from itertools import groupby
-
-import commons
-from ocitysmap2 import coords
-
-import psycopg2.extensions
-# compatibility with django: see http://code.djangoproject.com/ticket/5996
-psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
-# SQL string escaping routine
-_sql_escape_unicode = lambda s: psycopg2.extensions.adapt(s.encode('utf-8'))
-
-
-l = logging.getLogger('ocitysmap')
-
-
-class StreetIndex:
-
-    def __init__(self, db, polygon_wkt, i18n, page_number=None):
-        """
-        Prepare the index of the streets inside the given WKT. This
-        constructor will perform all the SQL queries.
-
-        Args:
-           db (psycopg2 DB): The GIS database
-           polygon_wkt (str): The WKT of the surrounding polygon of interest
-           i18n (i18n.i18n): Internationalization configuration
-
-        Note: All the arguments have to be provided !
-        """
-        self._i18n = i18n
-        self._page_number = page_number
-
-        # Build the contents of the index
-        self._categories = \
-            (self._list_streets(db, polygon_wkt)
-             + self._list_amenities(db, polygon_wkt)
-             + self._list_villages(db, polygon_wkt))
-
-    @property
-    def categories(self):
-        return self._categories
-
-    def apply_grid(self, grid):
-        """
-        Update the location_str field of the streets and amenities by
-        mapping them onto the given grid.
-
-        Args:
-           grid (ocitysmap2.Grid): the Grid object from which we
-           compute the location strings
-
-        Returns:
-           Nothing, but self._categories has been modified!
-        """
-        for category in self._categories:
-            for item in category.items:
-                item.update_location_str(grid)
-        self.group_identical_grid_locations()
-
-    def group_identical_grid_locations(self):
-        """
-        Group locations whith the same name and the same position on the grid.
-
-        Returns:
-           Nothing, but self._categories has been modified!
-        """
-        categories = []
-        for category in self._categories:
-            if category.is_street:
-                categories.append(category)
-                continue
-            grouped_items = []
-            sort_key = lambda item:(item.label, item.location_str)
-            items = sorted(category.items, key=sort_key)
-            for label, same_items in groupby(items, key=sort_key):
-                grouped_items.append(same_items.next())
-            category.items = grouped_items
-
-    def write_to_csv(self, title, output_filename):
-        # TODO: implement writing the index to CSV
-        try:
-            fd = open(output_filename, 'w')
-        except Exception,ex:
-            l.warning('error while opening destination file %s: %s'
-                      % (output_filename, ex))
-            return
-
-        l.debug("Creating CSV file %s..." % output_filename)
-        writer = csv.writer(fd)
-
-        # Try to treat indifferently unicode and str in CSV rows
-        def csv_writerow(row):
-            _r = []
-            for e in row:
-                if type(e) is unicode:
-                    _r.append(e.encode('UTF-8'))
-                else:
-                    _r.append(e)
-            return writer.writerow(_r)
-
-        copyright_notice = (u'© %(year)d MapOSMatic/ocitysmap authors. '
-                            u'Map data © %(year)d OpenStreetMap.org '
-                            u'and contributors (CC-BY-SA)' %
-                            {'year': datetime.date.today().year})
-        if title is not None:
-            csv_writerow(['# (UTF-8)', title, copyright_notice])
-        else:
-            csv_writerow(['# (UTF-8)', '', copyright_notice])
-
-        for category in self._categories:
-            csv_writerow(['%s' % category.name])
-            for item in category.items:
-                csv_writerow(['', item.label, item.location_str or '???'])
-
-        fd.close()
-
-    def _get_selected_amenities(self):
-        """
-        Return the kinds of amenities to retrieve from DB as a list of
-        string tuples:
-          1. Category, displayed headers in the final index
-          2. db_amenity, description string stored in the DB
-          3. Label, text to display in the index for this amenity
-
-        Note: This has to be a function because gettext() has to be
-        called, which takes i18n into account... It cannot be
-        statically defined as a class attribute for example.
-        """
-
-        # Make sure gettext is available...
-        try:
-            selected_amenities = [
-                (_(u"Places of worship"), "place_of_worship",
-                 _(u"Place of worship")),
-                (_(u"Education"), "kindergarten", _(u"Kindergarten")),
-                (_(u"Education"), "school", _(u"School")),
-                (_(u"Education"), "college", _(u"College")),
-                (_(u"Education"), "university", _(u"University")),
-                (_(u"Education"), "library", _(u"Library")),
-                (_(u"Public buildings"), "townhall", _(u"Town hall")),
-                (_(u"Public buildings"), "post_office", _(u"Post office")),
-                (_(u"Public buildings"), "public_building",
-                 _(u"Public building")),
-                (_(u"Public buildings"), "police", _(u"Police"))]
-        except NameError:
-            l.exception("i18n has to be initialized beforehand")
-            return []
-
-        return selected_amenities
-
-    def _convert_street_index(self, sl):
-        """Given a list of street names, do some cleanup and pass it
-        through the internationalization layer to get proper sorting,
-        filtering of common prefixes, etc.
-
-        Args:
-            sl (list of tuple): list tuples of the form (street_name,
-                                linestring_wkt) where linestring_wkt
-                                is a WKT for the linestring between
-                                the 2 most distant point of the
-                                street, in 4326 SRID
-
-        Returns the list of IndexCategory objects. Each IndexItem will
-        have its square location still undefined at that point
-        """
-
-        # Street prefixes are postfixed, a human readable label is
-        # built to represent the list of squares, and the list is
-        # alphabetically-sorted.
-        prev_locale = locale.getlocale(locale.LC_COLLATE)
-        locale.setlocale(locale.LC_COLLATE, self._i18n.language_code())
-        try:
-            sorted_sl = sorted([(self._i18n.user_readable_street(name),
-                                 linestring) for name,linestring in sl],
-                               lambda x,y: locale.strcoll(x[0].lower(),
-                                                          y[0].lower()))
-        finally:
-            locale.setlocale(locale.LC_COLLATE, prev_locale)
-
-        result = []
-        current_category = None
-        NUMBER_LIST = [str(i) for i in xrange(10)]
-        for street_name, linestring in sorted_sl:
-            # Create new category if needed
-            if (not current_category
-               or (not self._i18n.first_letter_equal(street_name[0],
-                                                     current_category.name)
-                   and (current_category.name != commons.NUMBER_CATEGORY_NAME
-                        or street_name[0] not in NUMBER_LIST))):
-                if street_name[0] in NUMBER_LIST:
-                    cat_name = commons.NUMBER_CATEGORY_NAME
-                else:
-                    cat_name = self._i18n.upper_unaccent_string(street_name[0])
-                current_category = commons.IndexCategory(cat_name)
-                result.append(current_category)
-
-            # Parse the WKT from the largest linestring in shape
-            try:
-                s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
-                                               linestring[11:-1].split(','))
-            except (ValueError, TypeError):
-                l.exception("Error parsing %s for %s" % (repr(linestring),
-                                                         repr(street_name)))
-                raise
-            endpoint1 = coords.Point(s_endpoint1[1], s_endpoint1[0])
-            endpoint2 = coords.Point(s_endpoint2[1], s_endpoint2[0])
-            current_category.items.append(commons.IndexItem(street_name,
-                                                            endpoint1,
-                                                            endpoint2,
-                                                            self._page_number))
-
-        return result
-
-    def _list_streets(self, db, polygon_wkt):
-        """Get the list of streets inside the given polygon. Don't
-        try to map them onto the grid of squares (there location_str
-        field remains undefined).
-
-        Args:
-           db (psycopg2 DB): The GIS database
-           polygon_wkt (str): The WKT of the surrounding polygon of interest
-
-        Returns a list of commons.IndexCategory objects, with their IndexItems
-        having no specific grid square location
-        """
-
-        cursor = db.cursor()
-        l.info("Getting streets...")
-
-        # PostGIS >= 1.5.0 for this to work:
-        query = """
-select name,
-       --- street_kind, -- only when group by is: group by name, street_kind
-       st_astext(st_transform(ST_LongestLine(street_path, street_path),
-                              4002)) as longest_linestring
-from
-  (select name,
-          -- highway as street_kind, -- only when group by name, street_kind
-          st_intersection(%(wkb_limits)s,
-                          st_linemerge(st_collect(%%(way)s))) as street_path
-   from planet_osm_line
-          where trim(name) != '' and highway is not null
-                and st_intersects(%%(way)s, %(wkb_limits)s)
-   group by name ---, street_kind -- (optional)
-   order by name) as foo;
-""" % dict(wkb_limits = ("st_transform(GeomFromText('%s', 4002), 900913)"
-                         % (polygon_wkt,)))
-
-        # l.debug("Street query (nogrid): %s" % query)
-
-        try:
-            cursor.execute(query % {'way':'way'})
-        except psycopg2.InternalError:
-            # This exception generaly occurs when inappropriate ways have
-            # to be cleaned. Using a buffer of 0 generaly helps to clean
-            # them. This operation is not applied by default for
-            # performance.
-            db.rollback()
-            cursor.execute(query % {'way':'st_buffer(way, 0)'})
-        sl = cursor.fetchall()
-
-        l.debug("Got %d streets." % len(sl))
-
-        return self._convert_street_index(sl)
-
-
-    def _list_amenities(self, db, polygon_wkt):
-        """Get the list of amenities inside the given polygon. Don't
-        try to map them onto the grid of squares (there location_str
-        field remains undefined).
-
-        Args:
-           db (psycopg2 DB): The GIS database
-           polygon_wkt (str): The WKT of the surrounding polygon of interest
-
-        Returns a list of commons.IndexCategory objects, with their IndexItems
-        having no specific grid square location
-        """
-
-        cursor = db.cursor()
-
-        result = []
-        for catname, db_amenity, label in self._get_selected_amenities():
-            l.info("Getting amenities for %s/%s..." % (catname, db_amenity))
-
-            # Get the current IndexCategory object, or create one if
-            # different than previous
-            if (not result or result[-1].name != catname):
-                current_category = commons.IndexCategory(catname,
-                                                         is_street=False)
-                result.append(current_category)
-            else:
-                current_category = result[-1]
-
-            query = """
-select amenity_name,
-       st_astext(st_transform(ST_LongestLine(amenity_contour, amenity_contour),
-                              4002)) as longest_linestring
-from (
-       select name as amenity_name,
-              st_intersection(%(wkb_limits)s, %%(way)s) as amenity_contour
-       from planet_osm_point
-       where trim(name) != ''
-             and amenity = %(amenity)s and ST_intersects(%%(way)s, 
%(wkb_limits)s)
-      union
-       select name as amenity_name,
-              st_intersection(%(wkb_limits)s , %%(way)s) as amenity_contour
-       from planet_osm_polygon
-       where trim(name) != '' and amenity = %(amenity)s
-             and ST_intersects(%%(way)s, %(wkb_limits)s)
-     ) as foo
-order by amenity_name""" \
-                % {'amenity': _sql_escape_unicode(db_amenity),
-                   'wkb_limits': ("st_transform(GeomFromText('%s' , 4002), 
900913)"
-                                  % (polygon_wkt,))}
-
-
-            # l.debug("Amenity query for for %s/%s (nogrid): %s" \
-            #             % (catname, db_amenity, query))
-            try:
-                cursor.execute(query % {'way':'way'})
-            except psycopg2.InternalError:
-                # This exception generaly occurs when inappropriate ways have
-                # to be cleaned. Using a buffer of 0 generaly helps to clean
-                # them. This operation is not applied by default for
-                # performance.
-                db.rollback()
-                cursor.execute(query % {'way':'st_buffer(way, 0)'})
-
-            for amenity_name, linestring in cursor.fetchall():
-                # Parse the WKT from the largest linestring in shape
-                try:
-                    s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
-                                                   
linestring[11:-1].split(','))
-                except (ValueError, TypeError):
-                    l.exception("Error parsing %s for %s/%s/%s"
-                                % (repr(linestring), catname, db_amenity,
-                                   repr(amenity_name)))
-                    continue
-                    ## raise
-                endpoint1 = coords.Point(s_endpoint1[1], s_endpoint1[0])
-                endpoint2 = coords.Point(s_endpoint2[1], s_endpoint2[0])
-                current_category.items.append(commons.IndexItem(amenity_name,
-                                                                endpoint1,
-                                                                endpoint2,
-                                                                
self._page_number))
-
-            l.debug("Got %d amenities for %s/%s."
-                    % (len(current_category.items), catname, db_amenity))
-
-        return [category for category in result if category.items]
-
-    def _list_villages(self, db, polygon_wkt):
-        """Get the list of villages inside the given polygon. Don't
-        try to map them onto the grid of squares (there location_str
-        field remains undefined).
-
-        Args:
-           db (psycopg2 DB): The GIS database
-           polygon_wkt (str): The WKT of the surrounding polygon of interest
-
-        Returns a list of commons.IndexCategory objects, with their IndexItems
-        having no specific grid square location
-        """
-
-        cursor = db.cursor()
-
-        result = []
-        current_category = commons.IndexCategory(_(u"Villages"),
-                                                 is_street=False)
-        result.append(current_category)
-
-        query = """
-select village_name,
-       st_astext(st_transform(ST_LongestLine(village_contour, village_contour),
-                              4002)) as longest_linestring
-from (
-       select name as village_name,
-              st_intersection(%(wkb_limits)s, %%(way)s) as village_contour
-       from planet_osm_point
-       where trim(name) != ''
-             and (place = 'locality'
-                  or place = 'hamlet'
-                  or place = 'isolated_dwelling')
-             and ST_intersects(%%(way)s, %(wkb_limits)s)
-     ) as foo
-order by village_name""" \
-            % {'wkb_limits': ("st_transform(GeomFromText('%s', 4002), 900913)"
-                              % (polygon_wkt,))}
-
-
-        # l.debug("Villages query for %s (nogrid): %s" \
-        #             % ('Villages', query))
-
-        try:
-            cursor.execute(query % {'way':'way'})
-        except psycopg2.InternalError:
-            # This exception generaly occurs when inappropriate ways have
-            # to be cleaned. Using a buffer of 0 generaly helps to clean
-            # them. This operation is not applied by default for
-            # performance.
-            db.rollback()
-            cursor.execute(query % {'way':'st_buffer(way, 0)'})
-
-        for village_name, linestring in cursor.fetchall():
-            # Parse the WKT from the largest linestring in shape
-            try:
-                s_endpoint1, s_endpoint2 = map(lambda s: s.split(),
-                                               linestring[11:-1].split(','))
-            except (ValueError, TypeError):
-                l.exception("Error parsing %s for %s/%s"
-                            % (repr(linestring), 'Villages',
-                               repr(village_name)))
-                continue
-                ## raise
-            endpoint1 = coords.Point(s_endpoint1[1], s_endpoint1[0])
-            endpoint2 = coords.Point(s_endpoint2[1], s_endpoint2[0])
-            current_category.items.append(commons.IndexItem(village_name,
-                                                            endpoint1,
-                                                            endpoint2,
-                                                            self._page_number))
-
-        l.debug("Got %d villages for %s."
-                % (len(current_category.items), 'Villages'))
-
-        return [category for category in result if category.items]
-
-if __name__ == "__main__":
-    import os
-    import psycopg2
-    from ocitysmap2 import i18n
-
-    logging.basicConfig(level=logging.DEBUG)
-
-    db = psycopg2.connect(user='maposmatic',
-                          password='waeleephoo3Aew3u',
-                          host='localhost',
-                          database='maposmatic')
-
-    i18n = i18n.install_translation("fr_FR.UTF-8",
-                                    os.path.join(os.path.dirname(__file__),
-                                                 "..", "..", "locale"))
-
-    # Chevreuse
-    chevreuse_bbox = coords.BoundingBox(48.7097, 2.0333, 48.7048, 2.0462)
-    limits_wkt = chevreuse_bbox.as_wkt()
-
-    # Paris envelope:
-    # limits_wkt = """POLYGON((2.22405967037499 
48.8543531620913,2.22407682819692 48.8550025657752,2.22423996225251 
48.8557367772146,2.22466908746374 48.8572219531993,2.22506398686264 
48.8582666566114,2.22559363355415 48.8594446700813,2.2256475324712 
48.8595315483635,2.22678057753906 48.861620896078,2.22753588102995 
48.8635801454558,2.22787059330481 48.8647094580464,2.22819677158448 
48.8653982612096,2.2290979614775 48.8666404585278,2.22973316021491 
48.8672502886549,2.23105485149243 48.8683285824972,2.23214657405722 
48.8695317674313,2.23752344019032 48.8710122798192,2.23998374609047 
48.8716393690211,2.2406936846595 48.8720829886714,2.24189536101507 
48.872839461003,2.24319136047547 48.8738725534154,2.24437587900911 
48.8749394788509,2.24560396583403 48.876357855343,2.2475739712521 
48.8757695828576,2.25479813293547 48.8740773968287,2.25538769725643 
48.8742418854232,2.25841672656296 48.8800895643967,2.25883381434937 
48.8802801449622,2.27745184777039 48.8779547879509,2.27972135150419 
48.8786284619886,2.2799882409751 48.8789087875894,2.28068147087986 
48.8818378450628,2.2818318534327 48.8837294922328,2.28231793183294 
48.8838550796657,2.28449203448356 48.8855611708289,2.28577384056247 
48.8864085830218,2.28619479110461 48.8867093618694,2.28716685807356 
48.8872081947562,2.28783807925385 48.8875526789321,2.28864323924301 
48.8879659741163,2.28891794405689 48.888090074233,2.28971618701836 
48.8884625499349,2.29060021908946 48.8888914901301,2.2910407529048 
48.889100761049,2.29231914538563 48.8897813727734,2.29280746957407 
48.889718645603,2.29453124677277 48.8896871638578,2.29575008095026 
48.8900483462911,2.29616537210611 48.8902642866599,2.29733794304647 
48.8910359587209,2.29849335616491 48.8916923874085,2.30047153625207 
48.8926164760274,2.30323852699021 48.8938978632484,2.30376898216549 
48.8941694805188,2.30599186333604 48.8952257558751,2.30716075118374 
48.895782794086,2.30753112657538 48.8959580779389,2.3095294289249 
48.8967071614408,2.31232139282795 48.8977543476603,2.31338886088006 
48.8980659834922,2.31527002291654 48.8986490922026,2.3161877418108 
48.8989256442189,2.31850782069509 48.8996320466728,2.3186937719589 
48.8997429489435,2.31986508525787 48.9004574890899,2.32026717117904 
48.9006974778028,2.32035754169662 48.9007570614361,2.32190785421396 
48.9008073738244,2.32408474164196 48.9008856768068,2.32755547257369 
48.9008753427098,2.3277815785307 48.9009897853332,2.33018241595904 
48.9010280509197,2.33437611103142 48.9011239509646,2.34387471718264 
48.9013669480994,2.34414043884369 48.9013294504412,2.34815474035383 
48.9014532811833,2.35198506689379 48.9014935541578,2.35509512423894 
48.9015322917683,2.35784441816599 48.901608644928,2.36560774868288 
48.9017625909672,2.37010498448976 48.9018541788921,2.37028114411698 
48.9018568952303,2.3715506432765 48.9018857710774,2.37597825964886 
48.9019807837163,2.37900028209617 48.9020473928421,2.38442916068422 
48.9021559867851,2.38618599588537 48.901825184925,2.38870406345829 
48.9013527757594,2.38942343433781 48.9012220947855,2.38942612928366 
48.901219496516,2.39066068397863 48.9009871870515,2.39373992910953 
48.8992668587557,2.39552739686187 48.8982616923999,2.39644098350582 
48.8967792698801,2.3969293975258 48.8959819963415,2.39776222562571 
48.8945922886365,2.39802974391732 48.893447763192,2.39801492171513 
48.8935286763732,2.39826231774438 48.8926270480788,2.39881388332883 
48.890253950367,2.39895932057332 48.8895456434227,2.39910251202961 
48.8886453608921,2.3991283835098 48.8875843982919,2.39955014253569 
48.8875104454888,2.39916467544727 48.8865343409336,2.39919692496597 
48.8858403943193,2.3992251320659 48.8853650577775,2.39924848826328 
48.8848527974081,2.40007305186258 48.8838178642316,2.4014665185313 
48.8826086429161,2.40379216697036 48.8814466523376,2.40460945421585 
48.8812171456678,2.40718249868415 48.8805096559317,2.40837555121299 
48.8803798656879,2.40929021583528 48.8802754779329,2.41081034495907 
48.8784304903174,2.41084259447777 48.8784022507851,2.41185625344437 
48.8771519560988,2.4124848944802 48.8763725664976,2.41283757306074 
48.8751571978536,2.41313805952328 48.8741215913749,2.41342911367534 
48.873149893187,2.41356727456603 48.8726744951149,2.41362404809199 
48.8724535746873,2.41370256084782 48.8716509499783,2.41378511602243 
48.8708747835672,2.41382086897074 48.8705644554421,2.41395444845349 
48.8692854838219,2.41400888635971 48.8676265832271,2.41392121078798 
48.865438090478,2.41427317071629 48.8635013703719,2.41435258178741 
48.8633375556219,2.41429544893534 48.86108297997,2.41449361728702 
48.8598641666786,2.4146241424978 48.8590365765406,2.41474819983854 
48.8587550757552,2.41485078744398 48.8577987414008,2.41516681476094 
48.8558674588656,2.41526688708359 48.8552628686639,2.41528063130744 
48.8551795884365,2.41534045910536 48.8548472936301,2.41542642787805 
48.8543619690239,2.41549595748104 48.8539192562004,2.41559863491801 
48.8534670821859,2.41571029550783 48.8527875722221,2.41588232288474 
48.8518013915656,2.41603791109195 48.8508464612523,2.41619754171794 
48.8498895620232,2.41633264833667 48.8492345945092,2.41641933576159 
48.8487604470666,2.41590693672352 48.8466070719205,2.4158173746897 
48.8459314888008,2.41570274965944 48.8452938500999,2.41508273245034 
48.8438678534372,2.41483057535009 48.842407466859,2.41397762498782 
48.8397444662905,2.41343683918678 48.8382643887995,2.41308496908999 
48.8372079747027,2.41286182757341 48.8365921613894,2.41272447516647 
48.8361966532922,2.41248094189295 48.8351168985142,2.41227567685053 
48.8345595985272,2.41120937660828 48.8338612648054,2.41185337883546 
48.8337666549157,2.41276912143609 48.8336280511047,2.41377514472278 
48.8335691561952,2.41456503335211 48.8335829929573,2.4152836855794 
48.8336421835075,2.41609872703668 48.8337632844351,2.41802022342941 
48.8342701548677,2.41971058329954 48.8348930923147,2.42037623492507 
48.8350979770248,2.42216028907934 48.835786478119,2.42174365045056 
48.836467342233,2.42157791128064 48.8368203945709,2.42146292692427 
48.8373379951919,2.42136842415638 48.8378079936814,2.42127904178561 
48.8382237692705,2.42101987782615 48.8390223774626,2.42081524160442 
48.8396772420515,2.42053451807814 48.8402191720905,2.42030571717527 
48.8406060755701,2.4200338869703 48.8408907527957,2.41983958137434 
48.8410638641368,2.41957143426203 48.841498295291,2.4194850163317 
48.841891574028,2.41949453847371 48.8421841662022,2.41943623781177 
48.8425948801975,2.41960179731864 48.8427473525507,2.4198688664526 
48.8430295936735,2.41994342662119 48.8433190458415,2.41981406922027 
48.8434354529104,2.42209974262919 48.8444911444825,2.42232683673301 
48.8443736757305,2.4224450550244 48.8442929786174,2.42254979858653 
48.8442016990887,2.42274715845445 48.8439993937374,2.42290418396612 
48.8437964963748,2.42304917205297 48.8435854987992,2.4236784419095 
48.8426801914628,2.42453121260871 48.8419447834817,2.42477411706154 
48.841896067273,2.42472228426964 48.8417684826086,2.4250133384217 
48.8417235500266,2.42756428433401 48.8415158545549,2.42824206321588 
48.8414465634173,2.43078968536165 48.8413390791936,2.43159969625334 
48.8412721527067,2.43229364481032 48.8412155725592,2.43289785167042 
48.8411423789194,2.43343522387338 48.8410234833686,2.43353817080494 
48.8411703438687,2.43713304890893 48.840876563315,2.43794862935538 
48.8445595446304,2.43937416587975 48.844428596988,2.44051242117626 
48.8443530432941,2.4406181528852 48.8448934447891,2.44079565998534 
48.8448677875258,2.44085872171828 48.8452092526994,2.44068292141718 
48.8452065924015,2.440762961309 48.8459171825144,2.44667486402532 
48.8457350430313,2.4465157723885 48.8449324627219,2.44773784050101 
48.8448136944047,2.4508502334659 48.8444805622559,2.45368262155673 
48.8440907918728,2.45684127775875 48.8436784939176,2.45827462962609 
48.8434694467977,2.4598624917223 48.8431756797296,2.46110701771692 
48.842915846081,2.46205977090726 48.8426616866754,2.46269487981313 
48.8424582518045,2.4632613574313 48.8421589805216,2.46424842626549 
48.8417239638795,2.46530412638739 48.8410234833686,2.46573325159861 
48.8406450968431,2.46645423944564 48.8400349425497,2.46722634143235 
48.8390912580791,2.46853994787231 48.8378656419534,2.46941544594822 
48.8370634676784,2.4696986847573 48.8366792567132,2.46979758927008 
48.8364964923528,2.46975833289216 48.8357040524303,2.46962951448042 
48.8352841759955,2.469376818391 48.8346450417393,2.46897805623638 
48.8341124531957,2.46894293210877 48.8340655624265,2.46873425346827 
48.8338930773407,2.4684829946833 48.833685349399,2.4681630147791 
48.83350044538,2.46768493138489 48.833309154447,2.46675804967473 
48.8328283516622,2.46638156573916 48.8325708887733,2.46606769437889 
48.8323229449658,2.46572795153843 48.8319670816639,2.46544597037075 
48.8315933574293,2.46527798541262 48.8312954995835,2.46511368354715 
48.8309088789962,2.46499097367934 48.8304478634571,2.46468033625409 
48.829476440867,2.46453741429239 48.8286916960156,2.46455591958724 
48.8276476765417,2.46476693384748 48.827591554113,2.46508278150138 
48.8275423508361,2.46522525430544 48.8276692620745,2.46540869028646 
48.8275586730823,2.46616695821778 48.827329451485,2.46581778306685 
48.8265121461304,2.46574735514857 48.8262822692052,2.46538704088811 
48.8251054862733,2.46518545893835 48.8250846684754,2.46527987187471 
48.8246208788162,2.46514233980472 48.8243022796837,2.46474366748162 
48.8233780527712,2.46468662446108 48.8233752730267,2.46292781296631 
48.8203007812736,2.46264268769513 48.8194471626414,2.4625531256613 
48.8193715712664,2.46253803396453 48.8192435743433,2.46101026916082 
48.8184906690057,2.45957682746195 48.8177797489504,2.45883877162452 
48.8174406994093,2.45808337830211 48.8173220433582,2.45637532162088 
48.8174689733421,2.45465019694926 48.8172375761677,2.45322654688698 
48.8173483653878,2.45083927401944 48.817916148904,2.44960858208019 
48.8180567481208,2.44732452563879 48.818110751838,2.44506122028045 
48.8180814727215,2.44269299169693 48.8180604745566,2.44198386161164 
48.8180874468456,2.43986159175291 48.8184555934794,2.43736292779013 
48.818326647826,2.43750971250756 48.8192294970277,2.43688735967872 
48.8195204471285,2.43617912790872 48.8196933957274,2.43516259433321 
48.8197602918961,2.43490801178169 48.81967677518,2.43452964138402 
48.8194661491895,2.43436533951855 48.8194927658336,2.43419852237029 
48.8193901438057,2.4343578835017 48.8202087482566,2.43249298097186 
48.8215368818771,2.43293315546108 48.8217982459121,2.43277855540069 
48.821881463367,2.43264317928737 48.8219974470946,2.43221531171754 
48.8217339548627,2.43039254017454 48.8229863438568,2.4305666336766 
48.8231762547516,2.43030064252097 48.8232780410172,2.42970838325415 
48.8234758760205,2.42917622127984 48.8236283474274,2.42852395455204 
48.8237809957994,2.427802966705 48.8239278477077,2.42714207615048 
48.8240239547446,2.42645531411577 48.8241030876619,2.42554235629252 
48.8241716930794,2.42465293432971 48.8241878981385,2.4234599716324 
48.8241482726199,2.42285055454365 48.8241143839052,2.42180338841696 
48.8240634620762,2.42091073251913 48.8240352510058,2.41995806916032 
48.8240860545781,2.41934865207157 48.824153950308,2.41861904039781 
48.8242838863918,2.41781226344114 48.8244929544404,2.4172629436449 
48.8246059750222,2.41659342926365 48.8247020807587,2.41596685435297 
48.8247641797519,2.41512576175245 48.8247641797519,2.41461938142679 
48.8247415875556,2.41388114592631 48.8247076992422,2.41260239411936 
48.8247020807587,2.41152082251728 48.8248094823974,2.41040502510288 
48.8250241667014,2.40984717131144 48.8252106988182,2.4095934870752 
48.8253025452268,2.40872427720629 48.8256969565131,2.40802907100791 
48.826103843296,2.40753122467745 48.8264315976597,2.40699043887641 
48.8268045915822,2.40610649663684 48.827482561693,2.40505924067861 
48.8283357477846,2.40415679314418 48.8288372916559,2.40312867130151 
48.8292981446041,2.40238181197429 48.8295467538789,2.40146121847113 
48.82936159804,2.40029439674858 48.828848586832,2.39983086606198 
48.8286903949966,2.39569089024358 48.8277253844162,2.39434153085531 
48.8274719758729,2.39341455931363 48.8270650410594,2.39270219529332 
48.8267656778066,2.39101857278782 48.8260457675067,2.39024997423073 
48.8257170643678,2.38881231045002 48.8250059510992,2.38717072909982 
48.8244232849018,2.38514421965038 48.8237067712519,2.38151349876655 
48.8224128807427,2.38074516970405 48.8216876439636,2.37905867258964 
48.8211390665207,2.37788565249164 48.8208040616663,2.37602497204364 
48.8201182529016,2.37415387113835 48.8195135859543,2.37390485814159 
48.8195361805072,2.37086046764371 48.8185493450421,2.36695261649473 
48.8172924680276,2.36696115048993 48.8171455966772,2.36538191222045 
48.8164108785601,2.36485657744229 48.8163676977867,2.36431408484222 
48.8162124833021,2.36334076023187 48.8160458519734,2.36025819133442 
48.8157374908686,2.35668163886222 48.8160636567673,2.35617166527543 
48.8159785959341,2.35590899788635 48.8159722666451,2.35584261238685 
48.8159954543165,2.35561435047316 48.8161225722528,2.35535788145954 
48.8162946452976,2.35511291088156 48.8164794945072,2.35462368837783 
48.8169598626578,2.35419456316661 48.8172876768224,2.3538119706871 
48.8174337787999,2.35343063584899 48.8175532626916,2.35333622291263 
48.8175815957113,2.35282121876025 48.817688953459,2.35222904932496 
48.8177341441997,2.35174836081642 48.8177284657843,2.35119904102018 
48.8176380842518,2.35048667699988 48.8174571432466,2.34984294426728 
48.8172593436366,2.34927646664911 48.8170559830563,2.34859832844113 
48.8167620610855,2.34742243373422 48.8161799497174,2.34697615070107 
48.816010360671,2.34665868607966 48.8159651683763,2.34602348734226 
48.8160273964993,2.34526818385137 48.8161007451382,2.34459372873605 
48.8160886781114,2.34447047987907 48.8155624582755,2.34444541688264 
48.8155255469646,2.34366963180328 48.815818293137,2.34304512301776 
48.8160522995573,2.34162892897234 48.8163212636258,2.34029852403656 
48.8164059689672,2.33958903462517 48.8164496820727,2.33873931819792 
48.8164892545265,2.33811445008628 48.8165226751843,2.33675377192543 
48.8165755566752,2.33618729430726 48.8165981525526,2.33513671458248 
48.8166802547645,2.33494276831264 48.8166885951114,2.33370246439986 
48.8167583937045,2.33211541078741 48.8169823992109,2.33203123864528 
48.8169932830019,2.33219437270088 48.8176466610404,2.33234672697307 
48.8182133764852,2.3292783514571 48.8188955440884,2.32736916198376 
48.8193461966713,2.32566811216175 48.8197292984568,2.32132996799168 
48.8206846446699,2.31988844145525 48.8210021430204,2.31418755299918 
48.8222493455562,2.31400735095318 48.8223115658781,2.30921708470061 
48.8234334701976,2.30687382928199 48.8239227614225,2.30459606104757 
48.8243880952424,2.30415274245486 48.8244955566963,2.30400101700337 
48.8245166704496,2.30126582662629 48.8251162500177,2.29764687367268 
48.8259108681886,2.29422959250036 48.8266900974691,2.29222069003049 
48.8271378412956,2.29090331066633 48.8276893691378,2.28938767311896 
48.8283238611162,2.28546904218657 48.829978742085,2.28336285217143 
48.8308616895553,2.27902309103384 48.8324594827402,2.27632769602384 
48.830228648962,2.27575996076428 48.8297178937999,2.27339568476801 
48.8283193666538,2.27272688903898 48.8279749477035,2.27230836394811 
48.8279610502545,2.26783708945293 48.8278866544835,2.26760478512046 
48.8279671414773,2.26729971724997 48.8315588824109,2.26967162892616 
48.8328133911768,2.27002951773535 48.8330080545344,2.26997543915525 
48.8330472591227,2.26779953987406 48.8345744993608,2.26692476045038 
48.8345170838228,2.26617763162858 48.8344520995247,2.26489681369648 
48.8342849966568,2.26296911892829 48.8339027748382,2.26278244901225 
48.8339252446421,2.25745831398633 48.8345391985692,2.25702927860663 
48.8345165516496,2.25675457379275 48.8344826699466,2.25520965116712 
48.8347538411894,2.25511523823076 48.8348385745105,2.25205108479663 
48.8384259797004,2.25170774869504 48.8385728478167,2.25113264725014 
48.8425497709943,2.25252988684306 48.8455598787296,2.25068663371158 
48.8456377952918,2.24249372882582 48.8477268953035,2.24156136739244 
48.8484739820425,2.24035744524866 48.8495405623492,2.23967867821998 
48.8500017568624,2.2380956670263 48.8503630485457,2.2312561639476 
48.8518493888202,2.22409811826915 48.853457152044,2.22405967037499 
48.8543531620913))"""
-
-    # Paris bbox
-    # limits_wkt = """POLYGON((2.22405964791711 
48.8155243047565,2.22405964791711 48.9021584078545,2.46979772401737 
48.9021584078545,2.46979772401737 48.8155243047565,2.22405964791711 
48.8155243047565))"""
-
-    street_index = StreetIndex(db, limits_wkt, i18n)
-
-    print "=> Got %d categories, total %d items" \
-        % (len(street_index.categories),
-           reduce(lambda r,cat: r+len(cat.items), street_index.categories, 0))
diff --git a/ocitysmap2/indexlib/multi_page_renderer.py 
b/ocitysmap2/indexlib/multi_page_renderer.py
deleted file mode 100644
index b3fda0b..0000000
--- a/ocitysmap2/indexlib/multi_page_renderer.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2012  David Mentré
-# Copyright (C) 2012  Thomas Petazzoni
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import cairo
-import ocitysmap2.layoutlib.commons as UTILS
-import pango
-import pangocairo
-import math
-import draw_utils
-from ocitysmap2.layoutlib.abstract_renderer import Renderer
-
-# FIXME: refactoring
-# We use the same 10mm as GRAYED_MARGIN_MM in the map multi-page renderer
-PAGE_NUMBER_MARGIN_PT  = UTILS.convert_mm_to_pt(10)
-
-class MultiPageStreetIndexRenderer:
-    """
-    The MultiPageStreetIndexRenderer class encapsulates all the logic
-    related to the rendering of the street index on multiple pages
-    """
-
-    # ctx: Cairo context
-    # surface: Cairo surface
-    def __init__(self, i18n, ctx, surface, index_categories, rendering_area,
-                 page_number):
-        self._i18n           = i18n
-        self.ctx            = ctx
-        self.surface        = surface
-        self.index_categories = index_categories
-        self.rendering_area_x = rendering_area[0]
-        self.rendering_area_y = rendering_area[1]
-        self.rendering_area_w = rendering_area[2]
-        self.rendering_area_h = rendering_area[3]
-        self.page_number      = page_number
-
-    def _create_layout_with_font(self, pc, font_desc):
-        layout = pc.create_layout()
-        layout.set_font_description(font_desc)
-        font = layout.get_context().load_font(font_desc)
-        font_metric = font.get_metrics()
-
-        fascent = float(font_metric.get_ascent()) / pango.SCALE
-        fheight = float((font_metric.get_ascent() + font_metric.get_descent())
-                        / pango.SCALE)
-        em = float(font_metric.get_approximate_char_width()) / pango.SCALE
-
-        return layout, fascent, fheight, em
-
-    def _draw_page_number(self):
-        self.ctx.save()
-        self.ctx.translate(Renderer.PRINT_SAFE_MARGIN_PT,
-                           Renderer.PRINT_SAFE_MARGIN_PT)
-        draw_utils.render_page_number(self.ctx, self.page_number,
-                                      self.rendering_area_w,
-                                      self.rendering_area_h,
-                                      PAGE_NUMBER_MARGIN_PT,
-                                      transparent_background = False)
-        self.ctx.restore()
-
-    def _new_page(self):
-        self.surface.show_page()
-        self.page_number = self.page_number + 1
-        self._draw_page_number()
-
-    def render(self, dpi = UTILS.PT_PER_INCH):
-        self.ctx.save()
-
-        # Create a PangoCairo context for drawing to Cairo
-        pc = pangocairo.CairoContext(self.ctx)
-
-        header_fd = pango.FontDescription("Georgia Bold 12")
-        label_column_fd  = pango.FontDescription("DejaVu 8")
-
-        header_layout, header_fascent, header_fheight, header_em = \
-            self._create_layout_with_font(pc, header_fd)
-        label_layout, label_fascent, label_fheight, label_em = \
-            self._create_layout_with_font(pc, label_column_fd)
-        column_layout, _, _, _ = \
-            self._create_layout_with_font(pc, label_column_fd)
-
-        # By OCitysmap's convention, the default resolution is 72 dpi,
-        # which maps to the default pangocairo resolution (96 dpi
-        # according to pangocairo docs). If we want to render with
-        # another resolution (different from 72), we have to scale the
-        # pangocairo resolution accordingly:
-        pangocairo.context_set_resolution(column_layout.get_context(),
-                                          96.*dpi/UTILS.PT_PER_INCH)
-        pangocairo.context_set_resolution(label_layout.get_context(),
-                                          96.*dpi/UTILS.PT_PER_INCH)
-        pangocairo.context_set_resolution(header_layout.get_context(),
-                                          96.*dpi/UTILS.PT_PER_INCH)
-
-        margin = label_em
-
-        # find largest label and location
-        max_label_drawing_width = 0.0
-        max_location_drawing_width = 0.0
-        for category in self.index_categories:
-            for street in category.items:
-                w = street.label_drawing_width(label_layout)
-                if w > max_label_drawing_width:
-                    max_label_drawing_width = w
-
-                w = street.location_drawing_width(label_layout)
-                if w > max_location_drawing_width:
-                    max_location_drawing_width = w
-
-        # No street to render, bail out
-        if max_label_drawing_width == 0.0:
-            return
-
-        # Find best number of columns
-        max_drawing_width = \
-            max_label_drawing_width + max_location_drawing_width + 2 * margin
-        max_drawing_height = self.rendering_area_h - PAGE_NUMBER_MARGIN_PT
-
-        columns_count = int(math.ceil(self.rendering_area_w / 
max_drawing_width))
-        # following test should not be needed. No time to prove it. ;-)
-        if columns_count == 0:
-            columns_count = 1
-
-        # We have now have several columns
-        column_width = self.rendering_area_w / columns_count
-
-        column_layout.set_width(int(UTILS.convert_pt_to_dots(
-                    (column_width - margin) * pango.SCALE, dpi)))
-        label_layout.set_width(int(UTILS.convert_pt_to_dots(
-                    (column_width - margin - max_location_drawing_width
-                     - 2 * label_em)
-                    * pango.SCALE, dpi)))
-        header_layout.set_width(int(UTILS.convert_pt_to_dots(
-                    (column_width - margin) * pango.SCALE, dpi)))
-
-        if not self._i18n.isrtl():
-            orig_offset_x = offset_x = margin/2.
-            orig_delta_x  = delta_x  = column_width
-        else:
-            orig_offset_x = offset_x = \
-                self.rendering_area_w - column_width + margin/2.
-            orig_delta_x  = delta_x  = - column_width
-
-        actual_n_cols = 0
-        offset_y = margin/2.
-
-        # page number of first page
-        self._draw_page_number()
-
-        for category in self.index_categories:
-            if ( offset_y + header_fheight + label_fheight
-                 + margin/2. > max_drawing_height ):
-                offset_y       = margin/2.
-                offset_x      += delta_x
-                actual_n_cols += 1
-
-                if actual_n_cols == columns_count:
-                    self._new_page()
-                    actual_n_cols = 0
-                    offset_y = margin / 2.
-                    offset_x = orig_offset_x
-                    delta_x  = orig_delta_x
-
-            category.draw(self._i18n.isrtl(), self.ctx, pc, header_layout,
-                          UTILS.convert_pt_to_dots(header_fascent, dpi),
-                          UTILS.convert_pt_to_dots(header_fheight, dpi),
-                          UTILS.convert_pt_to_dots(self.rendering_area_x
-                                                   + offset_x, dpi),
-                          UTILS.convert_pt_to_dots(self.rendering_area_y
-                                                   + offset_y
-                                                   + header_fascent, dpi))
-
-            offset_y += header_fheight
-
-            for street in category.items:
-                label_height = street.label_drawing_height(label_layout)
-                if ( offset_y + label_height + margin/2.
-                     > max_drawing_height ):
-                    offset_y       = margin/2.
-                    offset_x      += delta_x
-                    actual_n_cols += 1
-
-                    if actual_n_cols == columns_count:
-                        self._new_page()
-                        actual_n_cols = 0
-                        offset_y = margin / 2.
-                        offset_x = orig_offset_x
-                        delta_x  = orig_delta_x
-
-                street.draw(self._i18n.isrtl(), self.ctx, pc, column_layout,
-                            UTILS.convert_pt_to_dots(label_fascent, dpi),
-                            UTILS.convert_pt_to_dots(label_fheight, dpi),
-                            UTILS.convert_pt_to_dots(self.rendering_area_x
-                                                     + offset_x, dpi),
-                            UTILS.convert_pt_to_dots(self.rendering_area_y
-                                                     + offset_y
-                                                     + label_fascent, dpi),
-                            label_layout,
-                            UTILS.convert_pt_to_dots(label_height, dpi),
-                            
UTILS.convert_pt_to_dots(max_location_drawing_width,
-                                                     dpi))
-
-                offset_y += label_height
-
-
-        self.ctx.restore()
-
-
-if __name__ == '__main__':
-    import random
-    import string
-    import commons
-    import coords
-
-    width = 72*21./2.54
-    height = 72*29.7/2.54
-
-    surface = cairo.PDFSurface('/tmp/myindex_render.pdf', width, height)
-
-    random.seed(42)
-
-    def rnd_str(max_len, letters = string.letters + ' ' * 4):
-        return ''.join(random.choice(letters)
-                       for i in xrange(random.randint(1, max_len)))
-
-    class i18nMock:
-        def __init__(self, rtl):
-            self.rtl = rtl
-        def isrtl(self):
-            return self.rtl
-
-    streets = []
-    for i in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
-              'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
-              'Schools', 'Public buildings']:
-        items = []
-        for label, location_str in [(rnd_str(40).capitalize(),
-                                     '%s%d-%s%d' \
-                                         % (rnd_str(2,
-                                                    string.ascii_uppercase),
-                                            random.randint(1,19),
-                                            rnd_str(2,
-                                                    string.ascii_uppercase),
-                                            random.randint(1,19),
-                                            ))]*random.randint(1, 20):
-            item              = commons.IndexItem(label, None, None)
-            item.location_str = location_str
-            item.page_number  = random.randint(1, 100)
-            items.append(item)
-        streets.append(commons.IndexCategory(i, items))
-
-    ctxtmp = cairo.Context(surface)
-
-    rendering_area = \
-        (15, 15, width - 2 * 15, height - 2 * 15)
-
-    mpsir = MultiPageStreetIndexRenderer(i18nMock(False), ctxtmp, surface,
-                                         streets, rendering_area, 1)
-    mpsir.render()
-    surface.show_page()
-
-    mpsir2 = MultiPageStreetIndexRenderer(i18nMock(True), ctxtmp, surface,
-                                          streets, rendering_area,
-                                          mpsir.page_number + 1)
-    mpsir2.render()
-
-    surface.finish()
diff --git a/ocitysmap2/indexlib/renderer.py b/ocitysmap2/indexlib/renderer.py
deleted file mode 100644
index 29f7119..0000000
--- a/ocitysmap2/indexlib/renderer.py
+++ /dev/null
@@ -1,589 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import cairo
-import logging
-import math
-import pango
-import pangocairo
-
-import commons
-import ocitysmap2.layoutlib.commons as UTILS
-
-LOG = logging.getLogger('ocitysmap')
-
-
-class StreetIndexRenderingStyle:
-    """
-    The StreetIndexRenderingStyle class defines how the header and
-    label items should be drawn (font family, size, etc.).
-    """
-    __slots__ = ["header_font_spec", "label_font_spec"]
-    header_font_spec = None
-    label_font_spec  = None
-
-    def __init__(self, header_font_spec, label_font_spec):
-        """
-        Specify how the headers and label should be rendered. The
-        Pango Font Speficication strings below are of the form
-        "serif,monospace bold italic condensed 16". See
-        http://www.pygtk.org/docs/pygtk/class-pangofontdescription.html
-        for more details.
-
-        Args:
-           header_font_spec (str): Pango Font Specification for the headers.
-           label_font_spec (str): Pango Font Specification for the labels.
-        """
-        self.header_font_spec = header_font_spec
-        self.label_font_spec  = label_font_spec
-
-    def __str__(self):
-        return "Style(headers=%s, labels=%s)" % (repr(self.header_font_spec),
-                                                 repr(self.label_font_spec))
-
-
-class StreetIndexRenderingArea:
-    """
-    The StreetIndexRenderingArea class describes the parameters of the
-    Cairo area and its parameters (fonts) where the index should be
-    renedered. It is basically returned by
-    StreetIndexRenderer::precompute_occupation_area() and used by
-    StreetIndexRenderer::render(). All its attributes x,y,w,h may be
-    used by the global map rendering engines.
-    """
-
-    def __init__(self, street_index_rendering_style, x, y, w, h, n_cols):
-        """
-        Describes the Cairo area to use when rendering the index.
-
-        Args:
-             street_index_rendering_style (StreetIndexRenderingStyle):
-                   how to render the text inside the index
-             x (int): horizontal origin position (cairo units).
-             y (int): vertical origin position (cairo units).
-             w (int): width of area to use (cairo units).
-             h (int): height of area to use (cairo units).
-             n_cols (int): number of columns in the index.
-        """
-        self.rendering_style = street_index_rendering_style
-        self.x, self.y, self.w, self.h, self.n_cols = x, y, w, h, n_cols
-
-    def __str__(self):
-        return "Area(%s, %dx%d+%d+%d, n_cols=%d)" \
-            % (self.rendering_style,
-               self.w, self.h, self.x, self.y, self.n_cols)
-
-
-class StreetIndexRenderer:
-    """
-    The StreetIndex class encapsulates all the logic related to the querying 
and
-    rendering of the street index.
-    """
-
-    def __init__(self, i18n, index_categories,
-                 street_index_rendering_styles \
-                     = [ StreetIndexRenderingStyle('Georgia Bold 16',
-                                                   'DejaVu 12'),
-                         StreetIndexRenderingStyle('Georgia Bold 14',
-                                                   'DejaVu 10'),
-                         StreetIndexRenderingStyle('Georgia Bold 12',
-                                                   'DejaVu 8'),
-                         StreetIndexRenderingStyle('Georgia Bold 10',
-                                                   'DejaVu 7'),
-                         StreetIndexRenderingStyle('Georgia Bold 8',
-                                                   'DejaVu 6'),
-                         StreetIndexRenderingStyle('Georgia Bold 6',
-                                                   'DejaVu 5'),
-                         StreetIndexRenderingStyle('Georgia Bold 5',
-                                                   'DejaVu 4'),
-                         StreetIndexRenderingStyle('Georgia Bold 4',
-                                                   'DejaVu 3'),
-                         StreetIndexRenderingStyle('Georgia Bold 3',
-                                                   'DejaVu 2'),
-                         StreetIndexRenderingStyle('Georgia Bold 2',
-                                                   'DejaVu 2'),
-                         StreetIndexRenderingStyle('Georgia Bold 1',
-                                                   'DejaVu 1'), ] ):
-        self._i18n             = i18n
-        self._index_categories = index_categories
-        self._rendering_styles = street_index_rendering_styles
-
-    def precompute_occupation_area(self, surface, x, y, w, h,
-                                   freedom_direction, alignment):
-        """Prepare to render the street and amenities index at the
-        given (x,y) coordinates into the provided Cairo surface. The
-        index must not be larger than the provided width and height
-        (in pixels). Nothing will be drawn on surface.
-
-        Args:
-            surface (cairo.Surface): the cairo surface to render into.
-            x (int): horizontal origin position, in pixels.
-            y (int): vertical origin position, in pixels.
-            w (int): maximum usable width for the index, in dots (Cairo unit).
-            h (int): maximum usable height for the index, in dots (Cairo unit).
-            freedom_direction (string): freedom direction, can be 'width' or
-                'height'. See _compute_columns_split for more details.
-            alignment (string): 'top' or 'bottom' for a freedom_direction
-                of 'height', 'left' or 'right' for 'width'. Tells which side to
-                stick the index to.
-
-        Returns the actual graphical StreetIndexRenderingArea defining
-        how and where the index should be rendered. Raise
-        IndexDoesNotFitError when the provided area's surface is not
-        enough to hold the index.
-        """
-        if ((freedom_direction == 'height' and
-             alignment not in ('top', 'bottom')) or
-            (freedom_direction == 'width' and
-             alignment not in ('left', 'right'))):
-            raise ValueError, 'Incompatible freedom direction and alignment!'
-
-        if not self._index_categories:
-            raise commons.IndexEmptyError
-
-        LOG.debug("Determining index area within %dx%d+%d+%d aligned %s/%s..."
-                  % (w,h,x,y, alignment, freedom_direction))
-
-        # Create a PangoCairo context for drawing to Cairo
-        ctx = cairo.Context(surface)
-        pc  = pangocairo.CairoContext(ctx)
-
-        # Iterate over the rendering_styles until we find a suitable layout
-        rendering_style = None
-        for rs in self._rendering_styles:
-            LOG.debug("Trying index fit using %s..." % rs)
-            try:
-                n_cols, min_dimension \
-                    = self._compute_columns_split(pc, rs, w, h,
-                                                  freedom_direction)
-
-                # Great: index did fit OK !
-                rendering_style = rs
-                break
-
-            except commons.IndexDoesNotFitError:
-                # Index did not fit => try smaller...
-                LOG.debug("Index %s too large: should try a smaller one."
-                        % rs)
-                continue
-
-        # Index really did not fit with any of the rendering styles ?
-        if not rendering_style:
-            raise commons.IndexDoesNotFitError("Index does not fit in area")
-
-        # Realign at bottom/top left/right
-        if freedom_direction == 'height':
-            index_width  = w
-            index_height = min_dimension
-        elif freedom_direction == 'width':
-            index_width  = min_dimension
-            index_height = h
-
-        base_offset_x = 0
-        base_offset_y = 0
-        if alignment == 'bottom':
-            base_offset_y = h - index_height
-        if alignment == 'right':
-            base_offset_x = w - index_width
-
-        area = StreetIndexRenderingArea(rendering_style,
-                                        x+base_offset_x, y+base_offset_y,
-                                        index_width, index_height, n_cols)
-        LOG.debug("Will be able to render index in %s" % area)
-        return area
-
-
-    def render(self, ctx, rendering_area, dpi = UTILS.PT_PER_INCH):
-        """
-        Render the street and amenities index at the given (x,y)
-        coordinates into the provided Cairo surface. The index must
-        not be larger than the provided surface (use
-        precompute_occupation_area() to adjust it).
-
-        Args:
-            ctx (cairo.Context): the cairo context to use for the rendering.
-            rendering_area (StreetIndexRenderingArea): the result from
-                precompute_occupation_area().
-            dpi (number): resolution of the target device.
-        """
-
-        if not self._index_categories:
-            raise commons.IndexEmptyError
-
-        LOG.debug("Rendering the street index within %s at %sdpi..."
-                  % (rendering_area, dpi))
-
-        ##
-        ## In the following, the algorithm only manipulates values
-        ## expressed in 'pt'. Only the drawing-related functions will
-        ## translate them to cairo units
-        ##
-
-        ctx.save()
-        ctx.move_to(UTILS.convert_pt_to_dots(rendering_area.x, dpi),
-                    UTILS.convert_pt_to_dots(rendering_area.y, dpi))
-
-        # Create a PangoCairo context for drawing to Cairo
-        pc = pangocairo.CairoContext(ctx)
-
-        header_fd = pango.FontDescription(
-            rendering_area.rendering_style.header_font_spec)
-        label_fd  = pango.FontDescription(
-            rendering_area.rendering_style.label_font_spec)
-
-        header_layout, header_fascent, header_fheight, header_em = \
-                self._create_layout_with_font(pc, header_fd)
-        label_layout, label_fascent, label_fheight, label_em = \
-                self._create_layout_with_font(pc, label_fd)
-
-        #print "RENDER", header_layout, header_fascent, header_fheight, 
header_em
-        #print "RENDER", label_layout, label_fascent, label_fheight, label_em
-
-        # By OCitysmap's convention, the default resolution is 72 dpi,
-        # which maps to the default pangocairo resolution (96 dpi
-        # according to pangocairo docs). If we want to render with
-        # another resolution (different from 72), we have to scale the
-        # pangocairo resolution accordingly:
-        pangocairo.context_set_resolution(label_layout.get_context(),
-                                          96.*dpi/UTILS.PT_PER_INCH)
-        pangocairo.context_set_resolution(header_layout.get_context(),
-                                          96.*dpi/UTILS.PT_PER_INCH)
-        # All this is because we want pango to have the exact same
-        # behavior as with the default 72dpi resolution. If we instead
-        # decided to call cairo::scale, then pango might choose
-        # different font metrics which don't fit in the prepared
-        # layout anymore...
-
-        margin = label_em
-        column_width = int(rendering_area.w / rendering_area.n_cols)
-
-        label_layout.set_width(int(UTILS.convert_pt_to_dots(
-                    (column_width - margin) * pango.SCALE, dpi)))
-        header_layout.set_width(int(UTILS.convert_pt_to_dots(
-                    (column_width - margin) * pango.SCALE, dpi)))
-
-        if not self._i18n.isrtl():
-            offset_x = margin/2.
-            delta_x  = column_width
-        else:
-            offset_x = rendering_area.w - column_width + margin/2.
-            delta_x  = - column_width
-
-        actual_n_cols = 1
-        offset_y = margin/2.
-        for category in self._index_categories:
-            if ( offset_y + header_fheight + label_fheight
-                 + margin/2. > rendering_area.h ):
-                offset_y       = margin/2.
-                offset_x      += delta_x
-                actual_n_cols += 1
-
-            category.draw(self._i18n.isrtl(), ctx, pc, header_layout,
-                          UTILS.convert_pt_to_dots(header_fascent, dpi),
-                          UTILS.convert_pt_to_dots(header_fheight, dpi),
-                          UTILS.convert_pt_to_dots(rendering_area.x
-                                                   + offset_x, dpi),
-                          UTILS.convert_pt_to_dots(rendering_area.y
-                                                   + offset_y
-                                                   + header_fascent, dpi))
-
-            offset_y += header_fheight
-
-            for street in category.items:
-                if ( offset_y + label_fheight + margin/2.
-                     > rendering_area.h ):
-                    offset_y       = margin/2.
-                    offset_x      += delta_x
-                    actual_n_cols += 1
-
-                street.draw(self._i18n.isrtl(), ctx, pc, label_layout,
-                            UTILS.convert_pt_to_dots(label_fascent, dpi),
-                            UTILS.convert_pt_to_dots(label_fheight, dpi),
-                            UTILS.convert_pt_to_dots(rendering_area.x
-                                                     + offset_x, dpi),
-                            UTILS.convert_pt_to_dots(rendering_area.y
-                                                     + offset_y
-                                                     + label_fascent, dpi))
-
-                offset_y += label_fheight
-
-        # Restore original context
-        ctx.restore()
-
-        # Simple verification...
-        if actual_n_cols < rendering_area.n_cols:
-            LOG.warning("Rounding/security margin lost some space (%d actual 
cols vs. allocated %d" % (actual_n_cols, rendering_area.n_cols))
-        assert actual_n_cols <= rendering_area.n_cols
-
-
-    def _create_layout_with_font(self, pc, font_desc):
-        layout = pc.create_layout()
-        layout.set_font_description(font_desc)
-        font = layout.get_context().load_font(font_desc)
-        font_metric = font.get_metrics()
-
-        fascent = float(font_metric.get_ascent()) / pango.SCALE
-        fheight = float((font_metric.get_ascent() + font_metric.get_descent())
-                        / pango.SCALE)
-        em = float(font_metric.get_approximate_char_width()) / pango.SCALE
-
-        return layout, fascent, fheight, em
-
-
-    def _compute_lines_occupation(self, pc, font_desc, n_em_padding,
-                                  text_lines):
-        """Compute the visual dimension parameters of the initial long column
-        for the given text lines with the given font.
-
-        Args:
-            pc (pangocairo.CairoContext): the PangoCairo context.
-            font_desc (pango.FontDescription): Pango font description,
-                representing the used font at a given size.
-            n_em_padding (int): number of extra em space to account for.
-            text_lines (list): the list of text labels.
-
-        Returns a dictionnary with the following key,value pairs:
-            column_width: the computed column width (pixel size of the longest
-                label).
-            column_height: the total height of the column.
-            fascent: scaled font ascent.
-            fheight: scaled font height.
-        """
-
-        layout, fascent, fheight, em = self._create_layout_with_font(pc,
-                                                                     font_desc)
-        #print "PREPARE", layout, fascent, fheight, em
-
-        width = max(map(lambda x: self._label_width(layout, x), text_lines))
-        # Save some extra space horizontally
-        width += n_em_padding * em
-
-        height = fheight * len(text_lines)
-
-        return {'column_width': width, 'column_height': height,
-                'fascent': fascent, 'fheight': fheight, 'em': em}
-
-
-    def _label_width(self, layout, label):
-        layout.set_text(label)
-        return float(layout.get_size()[0]) / pango.SCALE
-
-    def _compute_column_occupation(self, pc, rendering_style):
-        """Returns the size of the tall column with all headers, labels and
-        squares for the given font sizes.
-
-        Args:
-            pc (pangocairo.CairoContext): the PangoCairo context.
-            rendering_style (StreetIndexRenderingStyle): how to render the
-                headers and labels.
-
-        Return a tuple (width of tall column, height of tall column,
-                        vertical margin to reserve after each small column).
-        """
-
-        header_fd = pango.FontDescription(rendering_style.header_font_spec)
-        label_fd  = pango.FontDescription(rendering_style.label_font_spec)
-
-        # Account for maximum square width (at worst " " + "Z99-Z99")
-        label_block = self._compute_lines_occupation(pc, label_fd, 1+7,
-                reduce(lambda x,y: x+y.get_all_item_labels(),
-                       self._index_categories, []))
-
-        # Reserve a small margin around the category headers
-        headers_block = self._compute_lines_occupation(pc, header_fd, 2,
-                [x.name for x in self._index_categories])
-
-        column_width = max(label_block['column_width'],
-                           headers_block['column_width'])
-        column_height = label_block['column_height'] + \
-                        headers_block['column_height']
-
-        # We make sure there will be enough space for a header and a
-        # label at the bottom of each column plus an additional
-        # vertical margin (arbitrary set to 1em, see render())
-        vertical_extra = ( label_block['fheight'] + headers_block['fheight']
-                           + label_block['em'] )
-        return column_width, column_height, vertical_extra
-
-
-    def _compute_columns_split(self, pc, rendering_style,
-                               zone_width_dots, zone_height_dots,
-                               freedom_direction):
-        """Computes the columns split for this index. From the one tall column
-        width and height it finds the number of columns fitting on the zone
-        dedicated to the index on the Cairo surface.
-
-        If the columns split does not fit on the index zone,
-        commons.IndexDoesNotFitError is raised.
-
-        Args:
-            pc (pangocairo.CairoContext): the PangoCairo context.
-            rendering_style (StreetIndexRenderingStyle): how to render the
-                headers and labels.
-            zone_width_dots (float): maximum width of the Cairo zone dedicated
-                to the index.
-            zone_height_dots (float): maximum height of the Cairo zone
-                dedicated to the index.
-            freedom_direction (string): the zone dimension that is flexible for
-                rendering this index, can be 'width' or 'height'. If the
-                streets don't fill the zone dedicated to the index, we need to
-                try with a zone smaller in the freedom_direction.
-
-        Returns a tuple (number of columns that will be in the index,
-                         the new value for the flexible dimension).
-        """
-
-        tall_width, tall_height, vertical_extra = \
-                self._compute_column_occupation(pc, rendering_style)
-
-        if zone_width_dots < tall_width:
-            raise commons.IndexDoesNotFitError
-
-        if freedom_direction == 'height':
-            n_cols = math.floor(zone_width_dots / float(tall_width))
-            if n_cols <= 0:
-                raise commons.IndexDoesNotFitError
-
-            min_required_height \
-                = math.ceil(float(tall_height + n_cols*vertical_extra)
-                            / n_cols)
-
-            LOG.debug("min req H %f vs. allocatable H %f"
-                      % (min_required_height, zone_height_dots))
-
-            if min_required_height > zone_height_dots:
-                raise commons.IndexDoesNotFitError
-
-            return int(n_cols), min_required_height
-
-        elif freedom_direction == 'width':
-            n_cols = math.ceil(float(tall_height) / zone_height_dots)
-            extra = n_cols * vertical_extra
-            min_required_width = n_cols * tall_width
-
-            if ( (min_required_width > zone_width_dots)
-                 or (tall_height + extra > n_cols * zone_height_dots) ):
-                raise commons.IndexDoesNotFitError
-
-            return int(n_cols), min_required_width
-
-        raise ValueError, 'Invalid freedom direction!'
-
-
-if __name__ == '__main__':
-    import random
-    import string
-
-    from ocitysmap2 import coords
-    import commons
-
-    logging.basicConfig(level=logging.DEBUG)
-
-    width = 72*21./2.54
-    height = .75 * 72*29.7/2.54
-
-    random.seed(42)
-
-    bbox = coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699)
-
-    surface = cairo.PDFSurface('/tmp/myindex_render.pdf', width, height)
-
-    def rnd_str(max_len, letters = string.letters):
-        return ''.join(random.choice(letters)
-                       for i in xrange(random.randint(1, max_len)))
-
-    class i18nMock:
-        def __init__(self, rtl):
-            self.rtl = rtl
-        def isrtl(self):
-            return self.rtl
-
-    streets = []
-    for i in ['A', 'B', # 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 
'M',
-              'N', 'O', # 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 
'Z',
-              'Schools', 'Public buildings']:
-        items = []
-        for label, location_str in [(rnd_str(10).capitalize(),
-                                     '%s%d-%s%d' \
-                                         % (rnd_str(2,
-                                                    string.ascii_uppercase),
-                                            random.randint(1,19),
-                                            rnd_str(2,
-                                                    string.ascii_uppercase),
-                                            random.randint(1,19),
-                                            ))]*4:
-            item              = commons.IndexItem(label, None, None)
-            item.location_str = location_str
-            items.append(item)
-        streets.append(commons.IndexCategory(i, items))
-
-    index = StreetIndexRenderer(i18nMock(False), streets)
-
-    def _render(freedom_dimension, alignment):
-        x,y,w,h = 50, 50, width-100, height-100
-
-        # Draw constraining rectangle
-        ctx = cairo.Context(surface)
-
-        ctx.save()
-        ctx.set_source_rgb(.2,0,0)
-        ctx.rectangle(x,y,w,h)
-        ctx.stroke()
-
-        # Precompute index area
-        rendering_area = index.precompute_occupation_area(surface, x,y,w,h,
-                                                          freedom_dimension,
-                                                          alignment)
-
-        # Draw a green background for the precomputed area
-        ctx.set_source_rgba(0,1,0,.5)
-        ctx.rectangle(rendering_area.x, rendering_area.y,
-                      rendering_area.w, rendering_area.h)
-        ctx.fill()
-        ctx.restore()
-
-        # Render the index
-        index.render(ctx, rendering_area)
-
-
-    _render('height', 'top')
-    surface.show_page()
-    _render('height', 'bottom')
-    surface.show_page()
-    _render('width', 'left')
-    surface.show_page()
-    _render('width', 'right')
-    surface.show_page()
-
-    index = StreetIndexRenderer(i18nMock(True), streets)
-    _render('height', 'top')
-    surface.show_page()
-    _render('height', 'bottom')
-    surface.show_page()
-    _render('width', 'left')
-    surface.show_page()
-    _render('width', 'right')
-
-    surface.finish()
-    print "Generated /tmp/myindex_render.pdf"
diff --git a/ocitysmap2/layoutlib/__init__.py b/ocitysmap2/layoutlib/__init__.py
deleted file mode 100644
index 83ed70e..0000000
--- a/ocitysmap2/layoutlib/__init__.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# Portrait paper sizes in milimeters
-PAPER_SIZES = [('A5', 148, 210),
-               ('A4', 210, 297),
-               ('A3', 297, 420),
-               ('A2', 420, 594),
-               ('A1', 594, 841),
-               ('A0', 841, 1189),
-
-               ('US letter', 216, 279),
-
-               ('100x75cm', 750, 1000),
-               ('80x60cm', 600, 800),
-               ('60x45cm', 450, 600),
-               ('40x30cm', 300, 400),
-
-               ('60x60cm', 600, 600),
-               ('50x50cm', 500, 500),
-               ('40x40cm', 400, 400),
-
-               ('Best fit', None, None),
-               ]
diff --git a/ocitysmap2/layoutlib/abstract_renderer.py 
b/ocitysmap2/layoutlib/abstract_renderer.py
deleted file mode 100644
index c01b419..0000000
--- a/ocitysmap2/layoutlib/abstract_renderer.py
+++ /dev/null
@@ -1,282 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2012  David Decotigny
-# Copyright (C) 2012  Frédéric Lehobey
-# Copyright (C) 2012  Pierre Mauduit
-# Copyright (C) 2012  David Mentré
-# Copyright (C) 2012  Maxime Petazzoni
-# Copyright (C) 2012  Thomas Petazzoni
-# Copyright (C) 2012  Gaël Utard
-# Copyright (C) 2012  Étienne Loks
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import math
-import os
-import sys
-import cairo
-import mapnik
-assert mapnik.mapnik_version >= 200100, \
-    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
-    "for more details." % mapnik.mapnik_version_string()
-import pango
-import re
-
-from ocitysmap2.maplib.map_canvas import MapCanvas
-from ocitysmap2.maplib.grid import Grid
-import commons
-from ocitysmap2 import maplib
-from ocitysmap2 import draw_utils
-import shapely.wkt
-
-import logging
-
-LOG = logging.getLogger('ocitysmap')
-
-class Renderer:
-    """
-    The job of an OCitySMap layout renderer is to lay out the resulting map and
-    render it from a given rendering configuration.
-    """
-    name = 'abstract'
-    description = 'The abstract interface of a renderer'
-
-    # The PRINT_SAFE_MARGIN_PT is a small margin we leave on all page borders
-    # to ease printing as printers often eat up margins with misaligned paper,
-    # etc.
-    PRINT_SAFE_MARGIN_PT = 15
-
-    GRID_LEGEND_MARGIN_RATIO = .02
-
-    # The DEFAULT_KM_IN_MM represents the minimum acceptable mapnik scale
-    # 12000 ensures that the zoom level will be 16 or higher
-    # see entities.xml.inc file from osm style sheet
-    DEFAULT_SCALE = 12000
-
-    def __init__(self, db, rc, tmpdir, dpi):
-        """
-        Create the renderer.
-
-        Args:
-           rc (RenderingConfiguration): rendering parameters.
-           tmpdir (os.path): Path to a temp dir that can hold temp files.
-           street_index (StreetIndex): None or the street index object.
-        """
-        # Note: street_index may be None
-        self.db           = db
-        self.rc           = rc
-        self.tmpdir       = tmpdir
-        self.grid         = None # The implementation is in charge of it
-
-        self.paper_width_pt = \
-                commons.convert_mm_to_pt(self.rc.paper_width_mm)
-        self.paper_height_pt = \
-                commons.convert_mm_to_pt(self.rc.paper_height_mm)
-
-    @staticmethod
-    def _get_osm_logo(ctx, height):
-        """
-        Read the OSM logo file and rescale it to fit within height.
-
-        Args:
-           ctx (cairo.Context): The cairo context to use to draw.
-           height (number): final height of the logo (cairo units).
-
-        Return a tuple (cairo group object for the logo, logo width in
-                        cairo units).
-        """
-        # TODO: read vector logo
-        logo_path = os.path.abspath(os.path.join(
-            os.path.dirname(__file__), '..', '..', 'images', 'osm-logo.png'))
-        if not os.path.exists(logo_path):
-            logo_path = os.path.join(
-                sys.exec_prefix, 'share', 'images', 'ocitysmap2',
-                'osm-logo.png')
-
-        try:
-            with open(logo_path, 'rb') as f:
-                png = cairo.ImageSurface.create_from_png(f)
-                LOG.debug('Using copyright logo: %s.' % logo_path)
-        except IOError:
-            LOG.warning('Cannot open logo from %s.' % logo_path)
-            return None, None
-
-        ctx.push_group()
-        ctx.save()
-        ctx.move_to(0, 0)
-        factor = height / png.get_height()
-        ctx.scale(factor, factor)
-        ctx.set_source_surface(png)
-        ctx.paint()
-        ctx.restore()
-        return ctx.pop_group(), png.get_width()*factor
-
-    @staticmethod
-    def _draw_labels(ctx, map_grid,
-                     map_area_width_dots, map_area_height_dots,
-                     grid_legend_margin_dots):
-        """
-        Draw the Grid labels at current position.
-
-        Args:
-           ctx (cairo.Context): The cairo context to use to draw.
-           map_grid (Grid): the grid objects whose labels we want to draw.
-           map_area_width_dots/map_area_height_dots (numbers): size of the
-              map (cairo units).
-           grid_legend_margin_dots (number): margin between border of
-              map and grid labels (cairo units).
-        """
-        ctx.save()
-
-        step_horiz = map_area_width_dots / map_grid.horiz_count
-        last_horiz_portion = math.modf(map_grid.horiz_count)[0]
-
-        step_vert = map_area_height_dots / map_grid.vert_count
-        last_vert_portion = math.modf(map_grid.vert_count)[0]
-
-        ctx.set_font_size(min(0.75 * grid_legend_margin_dots,
-                              0.5 * step_horiz))
-
-        for i, label in enumerate(map_grid.horizontal_labels):
-            x = i * step_horiz
-
-            if i < len(map_grid.horizontal_labels) - 1:
-                x += step_horiz/2.0
-            elif last_horiz_portion >= 0.3:
-                x += step_horiz * last_horiz_portion/2.0
-            else:
-                continue
-
-            draw_utils.draw_simpletext_center(ctx, label,
-                                         x, grid_legend_margin_dots/2.0)
-            draw_utils.draw_simpletext_center(ctx, label,
-                                         x, map_area_height_dots -
-                                         grid_legend_margin_dots/2.0)
-
-        for i, label in enumerate(map_grid.vertical_labels):
-            y = i * step_vert
-
-            if i < len(map_grid.vertical_labels) - 1:
-                y += step_vert/2.0
-            elif last_vert_portion >= 0.3:
-                y += step_vert * last_vert_portion/2.0
-            else:
-                continue
-
-            draw_utils.draw_simpletext_center(ctx, label,
-                                         grid_legend_margin_dots/2.0, y)
-            draw_utils.draw_simpletext_center(ctx, label,
-                                         map_area_width_dots -
-                                         grid_legend_margin_dots/2.0, y)
-
-        ctx.restore()
-
-    def _create_map_canvas(self, width, height, dpi,
-                           draw_contour_shade = True):
-        """
-        Create a new MapCanvas object.
-
-        Args:
-           graphical_ratio (float): ratio W/H of the area to render into.
-           draw_contour_shade (bool): whether to draw a shade around
-               the area of interest or not.
-
-        Return the MapCanvas object or raise ValueError.
-        """
-
-        # Prepare the map canvas
-        canvas = MapCanvas(self.rc.stylesheet,
-                           self.rc.bounding_box,
-                           width, height, dpi)
-
-        if draw_contour_shade:
-            # Area to keep visible
-            interior = shapely.wkt.loads(self.rc.polygon_wkt)
-
-            # Surroundings to gray-out
-            bounding_box \
-                = canvas.get_actual_bounding_box().create_expanded(0.05, 0.05)
-            exterior = shapely.wkt.loads(bounding_box.as_wkt())
-
-            # Determine the shade WKT
-            shade_wkt = exterior.difference(interior).wkt
-
-            # Prepare the shade SHP
-            shade_shape = maplib.shapes.PolyShapeFile(
-                canvas.get_actual_bounding_box(),
-                os.path.join(self.tmpdir, 'shade.shp'),
-                'shade')
-            shade_shape.add_shade_from_wkt(shade_wkt)
-
-            # Add the shade SHP to the map
-            canvas.add_shape_file(shade_shape,
-                                  self.rc.stylesheet.shade_color,
-                                  self.rc.stylesheet.shade_alpha,
-                                  self.rc.stylesheet.grid_line_width)
-
-        return canvas
-
-    def _create_grid(self, canvas):
-        """
-        Create a new Grid object for the given MapCanvas.
-
-        Args:
-           canvas (MapCanvas): Map Canvas (see _create_map_canvas).
-
-        Return a new Grid object.
-        """
-        # Prepare the grid SHP
-        map_grid = Grid(canvas.get_actual_bounding_box(), 
canvas.get_actual_scale(), self.rc.i18n.isrtl())
-        grid_shape = map_grid.generate_shape_file(
-            os.path.join(self.tmpdir, 'grid.shp'))
-
-        # Add the grid SHP to the map
-        canvas.add_shape_file(grid_shape,
-                              self.rc.stylesheet.grid_line_color,
-                              self.rc.stylesheet.grid_line_alpha,
-                              self.rc.stylesheet.grid_line_width)
-
-        return map_grid
-
-    # The next two methods are to be overloaded by the actual renderer.
-    def render(self, cairo_surface, dpi):
-        """Renders the map, the index and all other visual map features on the
-        given Cairo surface.
-
-        Args:
-            cairo_surface (Cairo.Surface): the destination Cairo device.
-            dpi (int): dots per inch of the device.
-        """
-        raise NotImplementedError
-
-    @staticmethod
-    def get_compatible_output_formats():
-        return [ "png", "svgz", "pdf", "csv" ]
-
-    @staticmethod
-    def get_compatible_paper_sizes(bounding_box, scale):
-        """Returns a list of the compatible paper sizes for the given bounding
-        box. The list is sorted, smaller papers first, and a "custom" paper
-        matching the dimensions of the bounding box is added at the end.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map geographic bounding box.
-            scale (int): minimum mapnik scale of the map.
-
-        Returns a list of tuples (paper name, width in mm, height in
-        mm, portrait_ok, landscape_ok, is_default). Paper sizes are
-        represented in portrait mode.
-        """
-        raise NotImplementedError
diff --git a/ocitysmap2/layoutlib/commons.py b/ocitysmap2/layoutlib/commons.py
deleted file mode 100644
index 75959ad..0000000
--- a/ocitysmap2/layoutlib/commons.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# PT/metrics conversion routines
-PT_PER_INCH = 72.0
-
-def convert_pt_to_dots(pt, dpi = PT_PER_INCH):
-    return float(pt * dpi) / PT_PER_INCH
-
-def convert_mm_to_pt(mm):
-    return ((mm/10.0) / 2.54) * 72
-
-def convert_pt_to_mm(pt):
-    return (float(pt) * 10.0 * 2.54) / 72
diff --git a/ocitysmap2/layoutlib/multi_page_renderer.py 
b/ocitysmap2/layoutlib/multi_page_renderer.py
deleted file mode 100644
index 24ff30c..0000000
--- a/ocitysmap2/layoutlib/multi_page_renderer.py
+++ /dev/null
@@ -1,787 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2012  David Mentré
-# Copyright (C) 2012  Thomas Petazzoni
-# Copyright (C) 2012  Gaël Utard
-# Copyright (C) 2012  Étienne Loks
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-import logging
-import tempfile
-import math
-import sys
-import cairo
-import mapnik
-assert mapnik.mapnik_version >= 200100, \
-    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
-    "for more details." % mapnik.mapnik_version_string()
-import coords
-import locale
-import pangocairo
-import pango
-import datetime
-
-from itertools import groupby
-
-from abstract_renderer import Renderer
-
-from ocitysmap2.maplib.map_canvas import MapCanvas
-from ocitysmap2.maplib.grid import Grid
-from ocitysmap2.maplib.overview_grid import OverviewGrid
-from indexlib.indexer import StreetIndex
-from indexlib.multi_page_renderer import MultiPageStreetIndexRenderer
-
-import ocitysmap2
-import commons
-import shapely.wkt
-from ocitysmap2 import maplib
-from ocitysmap2 import draw_utils
-
-from indexlib.commons import IndexCategory
-
-LOG = logging.getLogger('ocitysmap')
-
-class MultiPageRenderer(Renderer):
-    """
-    This Renderer creates a multi-pages map, with all the classic overlayed
-    features and no index page.
-    """
-
-    name = 'multi_page'
-    description = 'A multi-page layout.'
-    multipages = True
-
-    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
-        Renderer.__init__(self, db, rc, tmpdir, dpi)
-
-        self._grid_legend_margin_pt = \
-            min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt,
-                Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt)
-
-        # Compute the usable area per page
-        self._usable_area_width_pt = (self.paper_width_pt -
-                                      (2 * Renderer.PRINT_SAFE_MARGIN_PT))
-        self._usable_area_height_pt = (self.paper_height_pt -
-                                       (2 * Renderer.PRINT_SAFE_MARGIN_PT))
-
-        scale_denom = Renderer.DEFAULT_SCALE
-
-        # the mapnik scale depends on the latitude. However we are
-        # always using Mapnik conversion functions (lat,lon <->
-        # mercator_meters) so we don't need to take into account
-        # latitude in following computations
-
-        # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi
-        scale_denom *= float(72) / 90
-
-        GRAYED_MARGIN_MM  = 10
-        OVERLAP_MARGIN_MM = 20
-
-        # Debug: show original bounding box as JS code
-        # print self.rc.bounding_box.as_javascript("original", "#00ff00")
-
-        # Convert the original Bounding box into Mercator meters
-        self._proj = mapnik.Projection(coords._MAPNIK_PROJECTION)
-        orig_envelope = self._project_envelope(self.rc.bounding_box)
-
-        # Extend the bounding box to take into account the lost outter
-        # margin
-        off_x  = orig_envelope.minx - (GRAYED_MARGIN_MM * scale_denom) / 1000
-        off_y  = orig_envelope.miny - (GRAYED_MARGIN_MM * scale_denom) / 1000
-        width  = orig_envelope.width() + (2 * GRAYED_MARGIN_MM * scale_denom) 
/ 1000
-        height = orig_envelope.height() + (2 * GRAYED_MARGIN_MM * scale_denom) 
/ 1000
-
-        # Calculate the total width and height of paper needed to
-        # render the geographical area at the current scale.
-        total_width_pt   = commons.convert_mm_to_pt(float(width) * 1000 / 
scale_denom)
-        total_height_pt  = commons.convert_mm_to_pt(float(height) * 1000 / 
scale_denom)
-        self.grayed_margin_pt = commons.convert_mm_to_pt(GRAYED_MARGIN_MM)
-        overlap_margin_pt = commons.convert_mm_to_pt(OVERLAP_MARGIN_MM)
-
-        # Calculate the number of pages needed in both directions
-        if total_width_pt < self._usable_area_width_pt:
-            nb_pages_width = 1
-        else:
-            nb_pages_width = \
-                (float(total_width_pt - self._usable_area_width_pt) / \
-                     (self._usable_area_width_pt - overlap_margin_pt)) + 1
-
-        if total_height_pt < self._usable_area_height_pt:
-            nb_pages_height = 1
-        else:
-            nb_pages_height = \
-                (float(total_height_pt - self._usable_area_height_pt) / \
-                     (self._usable_area_height_pt - overlap_margin_pt)) + 1
-
-        # Round up the number of pages needed so that we have integer
-        # number of pages
-        self.nb_pages_width = int(math.ceil(nb_pages_width))
-        self.nb_pages_height = int(math.ceil(nb_pages_height))
-
-        # Calculate the entire paper area available
-        total_width_pt_after_extension = self._usable_area_width_pt + \
-            (self._usable_area_width_pt - overlap_margin_pt) * 
(self.nb_pages_width - 1)
-        total_height_pt_after_extension = self._usable_area_height_pt + \
-            (self._usable_area_height_pt - overlap_margin_pt) * 
(self.nb_pages_height - 1)
-
-        # Convert this paper area available in the number of Mercator
-        # meters that can be rendered on the map
-        total_width_merc = \
-            commons.convert_pt_to_mm(total_width_pt_after_extension) * 
scale_denom / 1000
-        total_height_merc = \
-            commons.convert_pt_to_mm(total_height_pt_after_extension) * 
scale_denom / 1000
-
-        # Extend the geographical boundaries so that we completely
-        # fill the available paper size. We are careful to extend the
-        # boundaries evenly on all directions (so the center of the
-        # previous boundaries remain the same as the new one)
-        off_x -= (total_width_merc - width) / 2
-        width = total_width_merc
-        off_y -= (total_height_merc - height) / 2
-        height = total_height_merc
-
-        # Calculate what is the final global bounding box that we will render
-        envelope = mapnik.Box2d(off_x, off_y, off_x + width, off_y + height)
-        self._geo_bbox = self._inverse_envelope(envelope)
-
-        # Debug: show transformed bounding box as JS code
-        # print self._geo_bbox.as_javascript("extended", "#0f0f0f")
-
-        # Convert the usable area on each sheet of paper into the
-        # amount of Mercator meters we can render in this area.
-        usable_area_merc_m_width  = 
commons.convert_pt_to_mm(self._usable_area_width_pt) * scale_denom / 1000
-        usable_area_merc_m_height = 
commons.convert_pt_to_mm(self._usable_area_height_pt) * scale_denom / 1000
-        grayed_margin_merc_m      = (GRAYED_MARGIN_MM * scale_denom) / 1000
-        overlap_margin_merc_m     = (OVERLAP_MARGIN_MM * scale_denom) / 1000
-
-        # Calculate all the bounding boxes that correspond to the
-        # geographical area that will be rendered on each sheet of
-        # paper.
-        area_polygon = shapely.wkt.loads(self.rc.polygon_wkt)
-        bboxes = []
-        self.page_disposition, map_number = {}, 0
-        for j in reversed(range(0, self.nb_pages_height)):
-            col = self.nb_pages_height - j - 1
-            self.page_disposition[col] = []
-            for i in range(0, self.nb_pages_width):
-                cur_x = off_x + i * (usable_area_merc_m_width - 
overlap_margin_merc_m)
-                cur_y = off_y + j * (usable_area_merc_m_height - 
overlap_margin_merc_m)
-                envelope = mapnik.Box2d(cur_x, cur_y,
-                                        cur_x+usable_area_merc_m_width,
-                                        cur_y+usable_area_merc_m_height)
-
-                envelope_inner = mapnik.Box2d(cur_x + grayed_margin_merc_m,
-                                              cur_y + grayed_margin_merc_m,
-                                              cur_x + usable_area_merc_m_width 
 - grayed_margin_merc_m,
-                                              cur_y + 
usable_area_merc_m_height - grayed_margin_merc_m)
-                inner_bb = self._inverse_envelope(envelope_inner)
-                if not area_polygon.disjoint(shapely.wkt.loads(
-                                                inner_bb.as_wkt())):
-                    self.page_disposition[col].append(map_number)
-                    map_number += 1
-                    bboxes.append((self._inverse_envelope(envelope),
-                                   inner_bb))
-                else:
-                    self.page_disposition[col].append(None)
-        # Debug: show per-page bounding boxes as JS code
-        # for i, (bb, bb_inner) in enumerate(bboxes):
-        #    print bb.as_javascript(name="p%d" % i)
-
-        self.pages = []
-
-        # Create an overview map
-
-        overview_bb = self._geo_bbox.create_expanded(0.001, 0.001)
-        # Create the overview grid
-        self.overview_grid = OverviewGrid(overview_bb,
-                     [bb_inner for bb, bb_inner in bboxes], 
self.rc.i18n.isrtl())
-
-        grid_shape = self.overview_grid.generate_shape_file(
-                    os.path.join(self.tmpdir, 'grid_overview.shp'))
-
-        # Create a canvas for the overview page
-        self.overview_canvas = MapCanvas(self.rc.stylesheet,
-                               overview_bb, self._usable_area_width_pt,
-                               self._usable_area_height_pt, dpi,
-                               extend_bbox_to_ratio=True)
-
-        # Create the gray shape around the overview map
-        exterior = 
shapely.wkt.loads(self.overview_canvas.get_actual_bounding_box()\
-                                                                .as_wkt())
-        interior = shapely.wkt.loads(self.rc.polygon_wkt)
-        shade_wkt = exterior.difference(interior).wkt
-        shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
-                os.path.join(self.tmpdir, 'shape_overview.shp'),
-                             'shade-overview')
-        shade.add_shade_from_wkt(shade_wkt)
-
-        self.overview_canvas.add_shape_file(shade)
-        self.overview_canvas.add_shape_file(grid_shape,
-                                  self.rc.stylesheet.grid_line_color, 1,
-                                  self.rc.stylesheet.grid_line_width)
-
-        self.overview_canvas.render()
-
-        # Create the map canvas for each page
-        indexes = []
-        for i, (bb, bb_inner) in enumerate(bboxes):
-
-            # Create the gray shape around the map
-            exterior = shapely.wkt.loads(bb.as_wkt())
-            interior = shapely.wkt.loads(bb_inner.as_wkt())
-            shade_wkt = exterior.difference(interior).wkt
-            shade = maplib.shapes.PolyShapeFile(
-                bb, os.path.join(self.tmpdir, 'shade%d.shp' % i),
-                'shade%d' % i)
-            shade.add_shade_from_wkt(shade_wkt)
-
-
-            # Create the contour shade
-
-            # Area to keep visible
-            interior_contour = shapely.wkt.loads(self.rc.polygon_wkt)
-            # Determine the shade WKT
-            shade_contour_wkt = interior.difference(interior_contour).wkt
-            # Prepare the shade SHP
-            shade_contour = maplib.shapes.PolyShapeFile(bb,
-                os.path.join(self.tmpdir, 'shade_contour%d.shp' % i),
-                'shade_contour%d' % i)
-            shade_contour.add_shade_from_wkt(shade_contour_wkt)
-
-
-            # Create one canvas for the current page
-            map_canvas = MapCanvas(self.rc.stylesheet,
-                                   bb, self._usable_area_width_pt,
-                                   self._usable_area_height_pt, dpi,
-                                   extend_bbox_to_ratio=False)
-
-            # Create the grid
-            map_grid = Grid(bb_inner, map_canvas.get_actual_scale(), 
self.rc.i18n.isrtl())
-            grid_shape = map_grid.generate_shape_file(
-                os.path.join(self.tmpdir, 'grid%d.shp' % i))
-
-            map_canvas.add_shape_file(shade)
-            map_canvas.add_shape_file(shade_contour,
-                                  self.rc.stylesheet.shade_color_2,
-                                  self.rc.stylesheet.shade_alpha_2)
-            map_canvas.add_shape_file(grid_shape,
-                                      self.rc.stylesheet.grid_line_color,
-                                      self.rc.stylesheet.grid_line_alpha,
-                                      self.rc.stylesheet.grid_line_width)
-
-            map_canvas.render()
-            self.pages.append((map_canvas, map_grid))
-
-            # Create the index for the current page
-            inside_contour_wkt = interior_contour.intersection(interior).wkt
-            index = StreetIndex(self.db,
-                                inside_contour_wkt,
-                                self.rc.i18n, page_number=(i + 4))
-
-            index.apply_grid(map_grid)
-            indexes.append(index)
-
-        # Merge all indexes
-        self.index_categories = self._merge_page_indexes(indexes)
-
-        # Prepare the small map for the front page
-        self._front_page_map = self._prepare_front_page_map(dpi)
-
-    def _merge_page_indexes(self, indexes):
-        # First, we split street categories and "other" categories,
-        # because we sort them and we don't want to have the "other"
-        # categories intermixed with the street categories. This
-        # sorting is required for the groupby Python operator to work
-        # properly.
-        all_categories_streets = []
-        all_categories_others  = []
-        for page_number, idx in enumerate(indexes):
-            for cat in idx.categories:
-                # Split in two lists depending on the category type
-                # (street or other)
-                if cat.is_street:
-                    all_categories_streets.append(cat)
-                else:
-                    all_categories_others.append(cat)
-
-        all_categories_streets_merged = \
-            self._merge_index_same_categories(all_categories_streets, 
is_street=True)
-        all_categories_others_merged = \
-            self._merge_index_same_categories(all_categories_others, 
is_street=False)
-
-        all_categories_merged = \
-            all_categories_streets_merged + all_categories_others_merged
-
-        return all_categories_merged
-
-    def _merge_index_same_categories(self, categories, is_street=True):
-        # Sort by categories. Now we may have several consecutive
-        # categories with the same name (i.e category for letter 'A'
-        # from page 1, category for letter 'A' from page 3).
-        categories.sort(key=lambda s:s.name)
-
-        categories_merged = []
-        for category_name,grouped_categories in groupby(categories,
-                                                        key=lambda s:s.name):
-
-            # Group the different IndexItem from categories having the
-            # same name. The groupby() function guarantees us that
-            # categories with the same name are grouped together in
-            # grouped_categories[].
-
-            grouped_items = []
-            for cat in grouped_categories:
-                grouped_items.extend(cat.items)
-
-            # Re-sort alphabetically all the IndexItem according to
-            # the street name.
-
-            prev_locale = locale.getlocale(locale.LC_COLLATE)
-            locale.setlocale(locale.LC_COLLATE, self.rc.i18n.language_code())
-            try:
-                grouped_items_sorted = \
-                    sorted(grouped_items,
-                           lambda x,y: locale.strcoll(x.label, y.label))
-            finally:
-                locale.setlocale(locale.LC_COLLATE, prev_locale)
-
-            self._blank_duplicated_names(grouped_items_sorted)
-
-            # Rebuild a IndexCategory object with the list of merged
-            # and sorted IndexItem
-            categories_merged.append(
-                IndexCategory(category_name, grouped_items_sorted, is_street))
-
-        return categories_merged
-
-    # We set the label to empty string in case of duplicated item. In
-    # multi-page renderer we won't draw the dots in that case
-    def _blank_duplicated_names(self, grouped_items_sorted):
-        prev_label = ''
-        for item in grouped_items_sorted:
-            if prev_label == item.label:
-                item.label = ''
-            else:
-                prev_label = item.label
-
-    def _project_envelope(self, bbox):
-        """Project the given bounding box into the rendering projection."""
-        envelope = mapnik.Box2d(bbox.get_top_left()[1],
-                                bbox.get_top_left()[0],
-                                bbox.get_bottom_right()[1],
-                                bbox.get_bottom_right()[0])
-        c0 = self._proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
-        c1 = self._proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
-        return mapnik.Box2d(c0.x, c0.y, c1.x, c1.y)
-
-    def _inverse_envelope(self, envelope):
-        """Inverse the given cartesian envelope (in 900913) back to a 4002
-        bounding box."""
-        c0 = self._proj.inverse(mapnik.Coord(envelope.minx, envelope.miny))
-        c1 = self._proj.inverse(mapnik.Coord(envelope.maxx, envelope.maxy))
-        return coords.BoundingBox(c0.y, c0.x, c1.y, c1.x)
-
-    def _prepare_front_page_map(self, dpi):
-        front_page_map_w = \
-            self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
-        front_page_map_h = \
-            (self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT) 
/ 2
-
-        # Create the nice small map
-        front_page_map = \
-            MapCanvas(self.rc.stylesheet,
-                      self.rc.bounding_box,
-                      front_page_map_w,
-                      front_page_map_h,
-                      dpi,
-                      extend_bbox_to_ratio=True)
-
-        # Add the shape that greys out everything that is outside of
-        # the administrative boundary.
-        exterior = 
shapely.wkt.loads(front_page_map.get_actual_bounding_box().as_wkt())
-        interior = shapely.wkt.loads(self.rc.polygon_wkt)
-        shade_wkt = exterior.difference(interior).wkt
-        shade = maplib.shapes.PolyShapeFile(self.rc.bounding_box,
-                os.path.join(self.tmpdir, 'shape_overview_cover.shp'),
-                             'shade-overview-cover')
-        shade.add_shade_from_wkt(shade_wkt)
-        front_page_map.add_shape_file(shade)
-        front_page_map.render()
-        return front_page_map
-
-    def _render_front_page_header(self, ctx, w, h):
-        # Draw a light blue block which will contain the name of the
-        # city being rendered.
-        blue_w = w
-        blue_h = 0.3 * h
-        ctx.set_source_rgb(.80,.80,.80)
-        ctx.rectangle(0, 0, blue_w, blue_h)
-        ctx.fill()
-        draw_utils.draw_text_adjusted(ctx, self.rc.title, blue_w/2, blue_h/2,
-                 blue_w, blue_h)
-
-    def _render_front_page_map(self, ctx, dpi, w, h):
-        # We will render the map slightly below the title
-        ctx.save()
-        ctx.translate(0, 0.3 * h + Renderer.PRINT_SAFE_MARGIN_PT)
-
-        # Render the map !
-        mapnik.render(self._front_page_map.get_rendered_map(), ctx)
-        ctx.restore()
-
-    def _render_front_page_footer(self, ctx, w, h, osm_date):
-        ctx.save()
-
-        # Draw the footer
-        ctx.translate(0, 0.8 * h + 2 * Renderer.PRINT_SAFE_MARGIN_PT)
-
-        # Display a nice grey rectangle as the background of the
-        # footer
-        footer_w = w
-        footer_h = 0.2 * h - 2 * Renderer.PRINT_SAFE_MARGIN_PT
-        ctx.set_source_rgb(.80,.80,.80)
-        ctx.rectangle(0, 0, footer_w, footer_h)
-        ctx.fill()
-
-        # Draw the OpenStreetMap logo to the right of the footer
-        logo_height = footer_h / 2
-        grp, logo_width = self._get_osm_logo(ctx, logo_height)
-        if grp:
-            ctx.save()
-            ctx.translate(w - logo_width - Renderer.PRINT_SAFE_MARGIN_PT,
-                          logo_height / 2)
-            ctx.set_source(grp)
-            ctx.paint_with_alpha(0.8)
-            ctx.restore()
-
-        # Prepare the text for the left of the footer
-        today = datetime.date.today()
-        notice = \
-            _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers.\n'
-              u'http://www.maposmatic.org\n\n'
-              u'Map data © %(year)d OpenStreetMap.org '
-              u'and contributors (cc-by-sa).\n'
-              u'http://www.openstreetmap.org\n\n'
-              u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s.\n'
-              u'The map may be incomplete or inaccurate. '
-              u'You can contribute to improve this map.\n'
-              u'See http://wiki.openstreetmap.org')
-
-        # We need the correct locale to be set for strftime().
-        prev_locale = locale.getlocale(locale.LC_TIME)
-        locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code())
-        try:
-            if osm_date is None:
-                osm_date_str = _(u'unknown')
-            else:
-                osm_date_str = osm_date.strftime("%d %B %Y %H:%M")
-
-            notice = notice % {'year': today.year,
-                               'date': today.strftime("%d %B %Y"),
-                               'osmdate': osm_date_str}
-        finally:
-            locale.setlocale(locale.LC_TIME, prev_locale)
-
-        draw_utils.draw_text_adjusted(ctx, notice,
-                Renderer.PRINT_SAFE_MARGIN_PT, footer_h/2, footer_w,
-                footer_h, align=pango.ALIGN_LEFT)
-        ctx.restore()
-
-    def _render_front_page(self, ctx, cairo_surface, dpi, osm_date):
-        # Draw a nice grey rectangle covering the whole page
-        ctx.save()
-        ctx.set_source_rgb(.95,.95,.95)
-        ctx.rectangle(Renderer.PRINT_SAFE_MARGIN_PT,
-                      Renderer.PRINT_SAFE_MARGIN_PT,
-                      self._usable_area_width_pt,
-                      self._usable_area_height_pt)
-        ctx.fill()
-        ctx.restore()
-
-        # Translate into the working area, taking another
-        # PRINT_SAFE_MARGIN_PT inside the grey area.
-        ctx.save()
-        ctx.translate(2 * Renderer.PRINT_SAFE_MARGIN_PT,
-                      2 * Renderer.PRINT_SAFE_MARGIN_PT)
-        w = self._usable_area_width_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
-        h = self._usable_area_height_pt - 2 * Renderer.PRINT_SAFE_MARGIN_PT
-
-        self._render_front_page_header(ctx, w, h)
-        self._render_front_page_map(ctx, dpi, w, h)
-        self._render_front_page_footer(ctx, w, h, osm_date)
-
-        ctx.restore()
-
-        cairo_surface.show_page()
-
-    def _render_blank_page(self, ctx, cairo_surface, dpi):
-        """
-        Render a blank page with a nice "intentionally blank" notice
-        """
-        ctx.save()
-        ctx.translate(
-                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
-                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
-
-        # footer notice
-        w = self._usable_area_width_pt
-        h = self._usable_area_height_pt
-        ctx.set_source_rgb(.6,.6,.6)
-        draw_utils.draw_simpletext_center(ctx, _('This page is intentionally 
left '\
-                                            'blank.'), w/2.0, 0.95*h)
-        draw_utils.render_page_number(ctx, 2,
-                                      self._usable_area_width_pt,
-                                      self._usable_area_height_pt,
-                                      self.grayed_margin_pt,
-                                      transparent_background=False)
-        cairo_surface.show_page()
-        ctx.restore()
-
-    def _render_overview_page(self, ctx, cairo_surface, dpi):
-        rendered_map = self.overview_canvas.get_rendered_map()
-        mapnik.render(rendered_map, ctx)
-
-        # draw pages numbers
-        self._draw_overview_labels(ctx, self.overview_canvas, 
self.overview_grid,
-              commons.convert_pt_to_dots(self._usable_area_width_pt),
-              commons.convert_pt_to_dots(self._usable_area_height_pt))
-        # Render the page number
-        draw_utils.render_page_number(ctx, 3,
-                                      self._usable_area_width_pt,
-                                      self._usable_area_height_pt,
-                                      self.grayed_margin_pt,
-                                      transparent_background = True)
-
-        cairo_surface.show_page()
-
-    def _draw_arrow(self, ctx, cairo_surface, number, max_digit_number,
-                    reverse_text=False):
-        arrow_edge = self.grayed_margin_pt*.6
-        ctx.save()
-        ctx.set_source_rgb(0, 0, 0)
-        ctx.translate(-arrow_edge/2, -arrow_edge*0.45)
-        ctx.line_to(0, 0)
-        ctx.line_to(0, arrow_edge)
-        ctx.line_to(arrow_edge, arrow_edge)
-        ctx.line_to(arrow_edge, 0)
-        ctx.line_to(arrow_edge/2, -arrow_edge*.25)
-        ctx.close_path()
-        ctx.fill()
-        ctx.restore()
-
-        ctx.save()
-        if reverse_text:
-            ctx.rotate(math.pi)
-        draw_utils.draw_text_adjusted(ctx, unicode(number), 0, 0, arrow_edge,
-                        arrow_edge, max_char_number=max_digit_number,
-                        text_color=(1, 1, 1, 1), width_adjust=0.85,
-                        height_adjust=0.9)
-        ctx.restore()
-
-    def _render_neighbour_arrows(self, ctx, cairo_surface, map_number,
-                                 max_digit_number):
-        nb_previous_pages = 4
-        current_line, current_col = None, None
-        for line_nb in xrange(self.nb_pages_height):
-            if map_number in self.page_disposition[line_nb]:
-                current_line = line_nb
-                current_col = self.page_disposition[line_nb].index(
-                                                             map_number)
-                break
-        if current_line == None:
-            # page not referenced
-            return
-
-        # north arrow
-        for line_nb in reversed(xrange(current_line)):
-            if self.page_disposition[line_nb][current_col] != None:
-                north_arrow = self.page_disposition[line_nb][current_col]
-                ctx.save()
-                ctx.translate(self._usable_area_width_pt/2,
-                    commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
-                self._draw_arrow(ctx, cairo_surface,
-                              north_arrow + nb_previous_pages, 
max_digit_number)
-                ctx.restore()
-                break
-
-        # south arrow
-        for line_nb in xrange(current_line + 1, self.nb_pages_height):
-            if self.page_disposition[line_nb][current_col] != None:
-                south_arrow = self.page_disposition[line_nb][current_col]
-                ctx.save()
-                ctx.translate(self._usable_area_width_pt/2,
-                     self._usable_area_height_pt \
-                      - commons.convert_pt_to_dots(self.grayed_margin_pt)/2)
-                ctx.rotate(math.pi)
-                self._draw_arrow(ctx, cairo_surface,
-                      south_arrow + nb_previous_pages, max_digit_number,
-                      reverse_text=True)
-                ctx.restore()
-                break
-
-        # west arrow
-        for col_nb in reversed(xrange(0, current_col)):
-            if self.page_disposition[current_line][col_nb] != None:
-                west_arrow = self.page_disposition[current_line][col_nb]
-                ctx.save()
-                ctx.translate(
-                    commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
-                    self._usable_area_height_pt/2)
-                ctx.rotate(-math.pi/2)
-                self._draw_arrow(ctx, cairo_surface,
-                               west_arrow + nb_previous_pages, 
max_digit_number)
-                ctx.restore()
-                break
-
-        # east arrow
-        for col_nb in xrange(current_col + 1, self.nb_pages_width):
-            if self.page_disposition[current_line][col_nb] != None:
-                east_arrow = self.page_disposition[current_line][col_nb]
-                ctx.save()
-                ctx.translate(
-                    self._usable_area_width_pt \
-                     - commons.convert_pt_to_dots(self.grayed_margin_pt)/2,
-                    self._usable_area_height_pt/2)
-                ctx.rotate(math.pi/2)
-                self._draw_arrow(ctx, cairo_surface,
-                               east_arrow + nb_previous_pages, 
max_digit_number)
-                ctx.restore()
-                break
-
-    def render(self, cairo_surface, dpi, osm_date):
-        ctx = cairo.Context(cairo_surface)
-
-        self._render_front_page(ctx, cairo_surface, dpi, osm_date)
-        self._render_blank_page(ctx, cairo_surface, dpi)
-
-        ctx.save()
-
-        # Prepare to draw the map at the right location
-        ctx.translate(
-                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT),
-                commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT))
-
-        self._render_overview_page(ctx, cairo_surface, dpi)
-
-        for map_number, (canvas, grid) in enumerate(self.pages):
-
-            rendered_map = canvas.get_rendered_map()
-            LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator())
-            LOG.debug('Actual scale: 1/%f' % canvas.get_actual_scale())
-            mapnik.render(rendered_map, ctx)
-
-            # Place the vertical and horizontal square labels
-            ctx.save()
-            ctx.translate(commons.convert_pt_to_dots(self.grayed_margin_pt),
-                      commons.convert_pt_to_dots(self.grayed_margin_pt))
-            self._draw_labels(ctx, grid,
-                  commons.convert_pt_to_dots(self._usable_area_width_pt) \
-                        - 2 * 
commons.convert_pt_to_dots(self.grayed_margin_pt),
-                  commons.convert_pt_to_dots(self._usable_area_height_pt) \
-                        - 2 * 
commons.convert_pt_to_dots(self.grayed_margin_pt),
-                  commons.convert_pt_to_dots(self._grid_legend_margin_pt))
-
-            ctx.restore()
-
-            # Render the page number
-            draw_utils.render_page_number(ctx, map_number+4,
-                                          self._usable_area_width_pt,
-                                          self._usable_area_height_pt,
-                                          self.grayed_margin_pt,
-                                          transparent_background = True)
-            self._render_neighbour_arrows(ctx, cairo_surface, map_number,
-                                          len(unicode(len(self.pages)+4)))
-
-            cairo_surface.show_page()
-        ctx.restore()
-
-        mpsir = MultiPageStreetIndexRenderer(self.rc.i18n,
-                                             ctx, cairo_surface,
-                                             self.index_categories,
-                                             (Renderer.PRINT_SAFE_MARGIN_PT,
-                                              Renderer.PRINT_SAFE_MARGIN_PT,
-                                              self._usable_area_width_pt,
-                                              self._usable_area_height_pt),
-                                              map_number+5)
-
-        mpsir.render()
-
-        cairo_surface.flush()
-
-    # In multi-page mode, we only render pdf format
-    @staticmethod
-    def get_compatible_output_formats():
-        return [ "pdf" ]
-
-    # In multi-page mode, we only accept A4, A5 and US letter as paper
-    # sizes. The goal is to render booklets, not posters.
-    # The default paper size is A4 portrait
-    @staticmethod
-    def get_compatible_paper_sizes(bounding_box,
-                                   scale=Renderer.DEFAULT_SCALE,
-                                   index_position=None, hsplit=1, vsplit=1):
-        valid_sizes = []
-        acceptable_formats = [ 'A5', 'A4', 'US letter' ]
-        for sz in ocitysmap2.layoutlib.PAPER_SIZES:
-            # Skip unsupported paper formats
-            if sz[0] not in acceptable_formats:
-                continue
-            valid_sizes.append((sz[0], sz[1], sz[2], True, True, sz[0] == 
'A4'))
-        return valid_sizes
-
-    @classmethod
-    def _draw_overview_labels(cls, ctx, map_canvas, overview_grid,
-                     area_width_dots, area_height_dots):
-        """
-        Draw the page numbers for the overview grid.
-
-        Args:
-           ctx (cairo.Context): The cairo context to use to draw.
-           overview_grid (OverViewGrid): the overview grid object
-           area_width_dots/area_height_dots (numbers): size of the
-              drawing area (cairo units).
-        """
-        ctx.save()
-        ctx.set_font_size(14)
-
-        bbox = map_canvas.get_actual_bounding_box()
-        bottom_right, bottom_left, top_left, top_right = bbox.to_mercator()
-        bottom, left = bottom_right.y, top_left.x
-        coord_delta_y = top_left.y - bottom_right.y
-        coord_delta_x = bottom_right.x - top_left.x
-        w, h = None, None
-        for idx, page_bb in enumerate(overview_grid._pages_bbox):
-            p_bottom_right, p_bottom_left, p_top_left, p_top_right = \
-                                                        page_bb.to_mercator()
-            center_x = p_top_left.x+(p_top_right.x-p_top_left.x)/2
-            center_y = p_bottom_left.y+(p_top_right.y-p_bottom_right.y)/2
-            y_percent = 100 - 100.0*(center_y - bottom)/coord_delta_y
-            y = int(area_height_dots*y_percent/100)
-
-            x_percent = 100.0*(center_x - left)/coord_delta_x
-            x = int(area_width_dots*x_percent/100)
-
-            if not w or not h:
-                w = area_width_dots*(p_bottom_right.x - p_bottom_left.x
-                                                         )/coord_delta_x
-                h = area_height_dots*(p_top_right.y - p_bottom_right.y
-                                                         )/coord_delta_y
-            draw_utils.draw_text_adjusted(ctx, unicode(idx+4), x, y, w, h,
-                 
max_char_number=len(unicode(len(overview_grid._pages_bbox)+3)),
-                 text_color=(0, 0, 0, 0.6))
-
-        ctx.restore()
diff --git a/ocitysmap2/layoutlib/renderers.py 
b/ocitysmap2/layoutlib/renderers.py
deleted file mode 100644
index 7c739cd..0000000
--- a/ocitysmap2/layoutlib/renderers.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import single_page_renderers
-import multi_page_renderer
-
-# The renderers registry
-_RENDERERS = [
-    single_page_renderers.SinglePageRendererIndexBottom,
-    single_page_renderers.SinglePageRendererIndexOnSide,
-    single_page_renderers.SinglePageRendererNoIndex,
-    multi_page_renderer.MultiPageRenderer,
-    ]
-
-def get_renderer_class_by_name(name):
-    """Retrieves a renderer class, by name."""
-    for renderer in _RENDERERS:
-        if renderer.name == name:
-            return renderer
-    raise LookupError, 'The requested renderer %s was not found!' % name
-
-def get_renderers():
-    """Returns the list of available renderers' names."""
-    return _RENDERERS
diff --git a/ocitysmap2/layoutlib/single_page_renderers.py 
b/ocitysmap2/layoutlib/single_page_renderers.py
deleted file mode 100644
index c223df8..0000000
--- a/ocitysmap2/layoutlib/single_page_renderers.py
+++ /dev/null
@@ -1,692 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import math
-import datetime
-import cairo
-import locale
-import mapnik
-assert mapnik.mapnik_version >= 200100, \
-    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
-    "for more details." % mapnik.mapnik_version_string()
-import pango
-import pangocairo
-
-import commons
-import ocitysmap2
-from abstract_renderer import Renderer
-from ocitysmap2.indexlib.renderer import StreetIndexRenderer
-
-import logging
-
-from indexlib.indexer import StreetIndex
-from indexlib.commons import IndexDoesNotFitError, IndexEmptyError
-import draw_utils
-
-LOG = logging.getLogger('ocitysmap')
-
-
-class SinglePageRenderer(Renderer):
-    """
-    This Renderer creates a full-page map, with the overlayed features
-    like the grid, grid labels, scale and compass rose and can draw an
-    index.
-    """
-
-    name = 'generic_single_page'
-    description = 'A generic full-page layout with or without index.'
-
-    MAX_INDEX_OCCUPATION_RATIO = 1/3.
-
-    def __init__(self, db, rc, tmpdir, dpi, file_prefix,
-                 index_position = 'side'):
-        """
-        Create the renderer.
-
-        Args:
-           rc (RenderingConfiguration): rendering parameters.
-           tmpdir (os.path): Path to a temp dir that can hold temp files.
-           index_position (str): None or 'side' (index on side),
-              'bottom' (index at bottom).
-        """
-        Renderer.__init__(self, db, rc, tmpdir, dpi)
-
-        # Prepare the index
-        self.street_index = StreetIndex(db,
-                                        rc.polygon_wkt,
-                                        rc.i18n)
-        if not self.street_index.categories:
-            LOG.warning("Designated area leads to an empty index")
-            self.street_index = None
-
-        self._grid_legend_margin_pt = \
-            min(Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_width_pt,
-                Renderer.GRID_LEGEND_MARGIN_RATIO * self.paper_height_pt)
-        self._title_margin_pt = 0.05 * self.paper_height_pt
-        self._copyright_margin_pt = 0.02 * self.paper_height_pt
-
-        self._usable_area_width_pt = (self.paper_width_pt -
-                                      2 * Renderer.PRINT_SAFE_MARGIN_PT)
-        self._usable_area_height_pt = (self.paper_height_pt -
-                                       (2 * Renderer.PRINT_SAFE_MARGIN_PT +
-                                        self._title_margin_pt +
-                                        self._copyright_margin_pt))
-
-        # Prepare the Index (may raise a IndexDoesNotFitError)
-        if ( index_position and self.street_index
-             and self.street_index.categories ):
-            self._index_renderer, self._index_area \
-                = self._create_index_rendering(index_position == "side")
-        else:
-            self._index_renderer, self._index_area = None, None
-
-        # Prepare the layout of the whole page
-        if not self._index_area:
-            # No index displayed
-            self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
-                                 ( Renderer.PRINT_SAFE_MARGIN_PT
-                                   + self._title_margin_pt ),
-                                 self._usable_area_width_pt,
-                                 self._usable_area_height_pt )
-        elif index_position == 'side':
-            # Index present, displayed on the side
-            if self._index_area.x > Renderer.PRINT_SAFE_MARGIN_PT:
-                # Index on the right -> map on the left
-                self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
-                                     ( Renderer.PRINT_SAFE_MARGIN_PT
-                                       + self._title_margin_pt ),
-                                     ( self._usable_area_width_pt
-                                       - self._index_area.w ),
-                                     self._usable_area_height_pt )
-            else:
-                # Index on the left -> map on the right
-                self._map_coords = ( self._index_area.x + self._index_area.w,
-                                     ( Renderer.PRINT_SAFE_MARGIN_PT
-                                       + self._title_margin_pt ),
-                                     ( self._usable_area_width_pt
-                                       - self._index_area.w ),
-                                     self._usable_area_height_pt )
-        elif index_position == 'bottom':
-            # Index present, displayed at the bottom -> map on top
-            self._map_coords = ( Renderer.PRINT_SAFE_MARGIN_PT,
-                                 ( Renderer.PRINT_SAFE_MARGIN_PT
-                                   + self._title_margin_pt ),
-                                 self._usable_area_width_pt,
-                                 ( self._usable_area_height_pt
-                                   - self._index_area.h ) )
-        else:
-            raise AssertionError("Invalid index position %s"
-                                 % repr(index_position))
-
-        # Prepare the map
-        self._map_canvas = self._create_map_canvas(
-            float(self._map_coords[2]),  # W
-            float(self._map_coords[3]),  # H
-            dpi )
-
-        # Prepare the grid
-        self.grid = self._create_grid(self._map_canvas)
-
-        # Update the street_index to reflect the grid's actual position
-        if self.grid and self.street_index:
-            self.street_index.apply_grid(self.grid)
-
-        # Dump the CSV street index
-        if self.street_index:
-            self.street_index.write_to_csv(rc.title, '%s.csv' % file_prefix)
-
-        # Commit the internal rendering stack of the map
-        self._map_canvas.render()
-
-
-    def _create_index_rendering(self, on_the_side):
-        """
-        Prepare to render the Street index.
-
-        Args:
-           on_the_side (bool): True=index on the side, False=at bottom.
-
-        Return a couple (StreetIndexRenderer, StreetIndexRenderingArea).
-        """
-        # Now we determine the actual occupation of the index
-        index_renderer = StreetIndexRenderer(self.rc.i18n,
-                                             self.street_index.categories)
-
-        # We use a fake vector device to determine the actual
-        # rendering characteristics
-        fake_surface = cairo.PDFSurface(None,
-                                        self.paper_width_pt,
-                                        self.paper_height_pt)
-
-        if on_the_side:
-            index_max_width_pt \
-                = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_width_pt
-
-            if not self.rc.i18n.isrtl():
-                # non-RTL: Index is on the right
-                index_area = index_renderer.precompute_occupation_area(
-                    fake_surface,
-                    ( self.paper_width_pt - Renderer.PRINT_SAFE_MARGIN_PT
-                      - index_max_width_pt ),
-                    ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ),
-                    index_max_width_pt,
-                    self._usable_area_height_pt,
-                    'width', 'right')
-            else:
-                # RTL: Index is on the left
-                index_area = index_renderer.precompute_occupation_area(
-                    fake_surface,
-                    Renderer.PRINT_SAFE_MARGIN_PT,
-                    ( Renderer.PRINT_SAFE_MARGIN_PT + self._title_margin_pt ),
-                    index_max_width_pt,
-                    self._usable_area_height_pt,
-                    'width', 'left')
-        else:
-            # Index at the bottom of the page
-            index_max_height_pt \
-                = self.MAX_INDEX_OCCUPATION_RATIO * self._usable_area_height_pt
-
-            index_area = index_renderer.precompute_occupation_area(
-                fake_surface,
-                Renderer.PRINT_SAFE_MARGIN_PT,
-                ( self.paper_height_pt
-                  - Renderer.PRINT_SAFE_MARGIN_PT
-                  - self._copyright_margin_pt
-                  - index_max_height_pt ),
-                self._usable_area_width_pt,
-                index_max_height_pt,
-                'height', 'bottom')
-
-        return index_renderer, index_area
-
-
-    def _draw_title(self, ctx, w_dots, h_dots, font_face):
-        """
-        Draw the title at the current position inside a
-        w_dots*h_dots rectangle.
-
-        Args:
-           ctx (cairo.Context): The Cairo context to use to draw.
-           w_dots,h_dots (number): Rectangle dimension (ciaro units)
-           font_face (str): Pango font specification.
-        """
-
-        # Title background
-        ctx.save()
-        ctx.set_source_rgb(0.8, 0.9, 0.96)
-        ctx.rectangle(0, 0, w_dots, h_dots)
-        ctx.fill()
-        ctx.restore()
-
-        # Retrieve and paint the OSM logo
-        ctx.save()
-        grp, logo_width = self._get_osm_logo(ctx, 0.8*h_dots)
-        if grp:
-            ctx.translate(w_dots - logo_width - 0.1*h_dots, 0.1*h_dots)
-            ctx.set_source(grp)
-            ctx.paint_with_alpha(0.5)
-        else:
-            LOG.warning("OSM Logo not available.")
-            logo_width = 0
-        ctx.restore()
-
-        # Prepare the title
-        pc = pangocairo.CairoContext(ctx)
-        layout = pc.create_layout()
-        layout.set_width(int((w_dots - 0.1*w_dots - logo_width) * pango.SCALE))
-        if not self.rc.i18n.isrtl(): layout.set_alignment(pango.ALIGN_LEFT)
-        else:                        layout.set_alignment(pango.ALIGN_RIGHT)
-        fd = pango.FontDescription(font_face)
-        fd.set_size(pango.SCALE)
-        layout.set_font_description(fd)
-        layout.set_text(self.rc.title)
-        draw_utils.adjust_font_size(layout, fd, layout.get_width(), 0.8*h_dots)
-
-        # Draw the title
-        ctx.save()
-        ctx.rectangle(0, 0, w_dots, h_dots)
-        ctx.stroke()
-        ctx.translate(0.1*h_dots,
-                      (h_dots -
-                       (layout.get_size()[1] / pango.SCALE)) / 2.0)
-        pc.show_layout(layout)
-        ctx.restore()
-
-
-    def _draw_copyright_notice(self, ctx, w_dots, h_dots, notice=None,
-                               osm_date=None):
-        """
-        Draw a copyright notice at current location and within the
-        given w_dots*h_dots rectangle.
-
-        Args:
-           ctx (cairo.Context): The Cairo context to use to draw.
-           w_dots,h_dots (number): Rectangle dimension (ciaro units).
-           font_face (str): Pango font specification.
-           notice (str): Optional notice to replace the default.
-        """
-
-        today = datetime.date.today()
-        notice = notice or \
-            _(u'Copyright © %(year)d MapOSMatic/OCitySMap developers. '
-              u'Map data © %(year)d OpenStreetMap.org '
-              u'and contributors (cc-by-sa).\n'
-              u'Map rendered on: %(date)s. OSM data updated on: %(osmdate)s. '
-              u'The map may be incomplete or inaccurate. '
-              u'You can contribute to improve this map. '
-              u'See http://wiki.openstreetmap.org')
-
-        # We need the correct locale to be set for strftime().
-        prev_locale = locale.getlocale(locale.LC_TIME)
-        locale.setlocale(locale.LC_TIME, self.rc.i18n.language_code())
-        try:
-            if osm_date is None:
-                osm_date_str = _(u'unknown')
-            else:
-                osm_date_str = osm_date.strftime("%d %B %Y %H:%M")
-
-            notice = notice % {'year': today.year,
-                               'date': today.strftime("%d %B %Y"),
-                               'osmdate': osm_date_str}
-        finally:
-            locale.setlocale(locale.LC_TIME, prev_locale)
-
-        ctx.save()
-        pc = pangocairo.CairoContext(ctx)
-        fd = pango.FontDescription('DejaVu')
-        fd.set_size(pango.SCALE)
-        layout = pc.create_layout()
-        layout.set_font_description(fd)
-        layout.set_text(notice)
-        draw_utils.adjust_font_size(layout, fd, w_dots, h_dots)
-        pc.show_layout(layout)
-        ctx.restore()
-
-
-    def render(self, cairo_surface, dpi, osm_date):
-        """Renders the map, the index and all other visual map features on the
-        given Cairo surface.
-
-        Args:
-            cairo_surface (Cairo.Surface): the destination Cairo device.
-            dpi (int): dots per inch of the device.
-        """
-        LOG.info('SinglePageRenderer rendering on %dx%dmm paper at %d dpi.' %
-                 (self.rc.paper_width_mm, self.rc.paper_height_mm, dpi))
-
-        # First determine some useful drawing parameters
-        safe_margin_dots \
-            = commons.convert_pt_to_dots(Renderer.PRINT_SAFE_MARGIN_PT, dpi)
-        usable_area_width_dots \
-            = commons.convert_pt_to_dots(self._usable_area_width_pt, dpi)
-        usable_area_height_dots \
-            = commons.convert_pt_to_dots(self._usable_area_height_pt, dpi)
-
-        title_margin_dots \
-            = commons.convert_pt_to_dots(self._title_margin_pt, dpi)
-
-        copyright_margin_dots \
-            = commons.convert_pt_to_dots(self._copyright_margin_pt, dpi)
-
-        map_coords_dots = map(lambda l: commons.convert_pt_to_dots(l, dpi),
-                              self._map_coords)
-
-        ctx = cairo.Context(cairo_surface)
-
-        # Set a white background
-        ctx.save()
-        ctx.set_source_rgb(1, 1, 1)
-        ctx.rectangle(0, 0, commons.convert_pt_to_dots(self.paper_width_pt, 
dpi),
-                      commons.convert_pt_to_dots(self.paper_height_pt, dpi))
-        ctx.fill()
-        ctx.restore()
-
-        ##
-        ## Draw the index, when applicable
-        ##
-        if self._index_renderer and self._index_area:
-            ctx.save()
-
-            # NEVER use ctx.scale() here because otherwise pango will
-            # choose different dont metrics which may be incompatible
-            # with what has been computed by __init__(), which may
-            # require more columns than expected !  Instead, we have
-            # to trick pangocairo into believing it is rendering to a
-            # device with the same default resolution, but with a
-            # cairo resolution matching the 'dpi' specified
-            # resolution. See
-            # index::render::StreetIndexRenederer::render() and
-            # comments within.
-
-            self._index_renderer.render(ctx, self._index_area, dpi)
-
-            ctx.restore()
-
-            # Also draw a rectangle
-            ctx.save()
-            ctx.rectangle(commons.convert_pt_to_dots(self._index_area.x, dpi),
-                          commons.convert_pt_to_dots(self._index_area.y, dpi),
-                          commons.convert_pt_to_dots(self._index_area.w, dpi),
-                          commons.convert_pt_to_dots(self._index_area.h, dpi))
-            ctx.stroke()
-            ctx.restore()
-
-
-        ##
-        ## Draw the map, scaled to fit the designated area
-        ##
-        ctx.save()
-
-        # Prepare to draw the map at the right location
-        ctx.translate(map_coords_dots[0], map_coords_dots[1])
-
-        # Draw the rescaled Map
-        ctx.save()
-        rendered_map = self._map_canvas.get_rendered_map()
-        LOG.debug('Mapnik scale: 1/%f' % rendered_map.scale_denominator())
-        LOG.debug('Actual scale: 1/%f' % self._map_canvas.get_actual_scale())
-        mapnik.render(rendered_map, ctx)
-        ctx.restore()
-
-        # Draw a rectangle around the map
-        ctx.rectangle(0, 0, map_coords_dots[2], map_coords_dots[3])
-        ctx.stroke()
-
-        # Place the vertical and horizontal square labels
-        self._draw_labels(ctx, self.grid,
-                          map_coords_dots[2],
-                          map_coords_dots[3],
-                          
commons.convert_pt_to_dots(self._grid_legend_margin_pt,
-                                                   dpi))
-        ctx.restore()
-
-        ##
-        ## Draw the title
-        ##
-        ctx.save()
-        ctx.translate(safe_margin_dots, safe_margin_dots)
-        self._draw_title(ctx, usable_area_width_dots,
-                         title_margin_dots, 'Georgia Bold')
-        ctx.restore()
-
-        ##
-        ## Draw the copyright notice
-        ##
-        ctx.save()
-
-        # Move to the right position
-        ctx.translate(safe_margin_dots,
-                      ( safe_margin_dots + title_margin_dots
-                        + usable_area_height_dots
-                        + copyright_margin_dots/4. ) )
-
-        # Draw the copyright notice
-        self._draw_copyright_notice(ctx, usable_area_width_dots,
-                                    copyright_margin_dots,
-                                    osm_date=osm_date)
-        ctx.restore()
-
-        # TODO: map scale
-        # TODO: compass rose
-
-        cairo_surface.flush()
-
-    @staticmethod
-    def _generic_get_compatible_paper_sizes(bounding_box,
-                                            scale=Renderer.DEFAULT_SCALE, 
index_position = None):
-        """Returns a list of the compatible paper sizes for the given bounding
-        box. The list is sorted, smaller papers first, and a "custom" paper
-        matching the dimensions of the bounding box is added at the end.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map geographic bounding box.
-            scale (int): minimum mapnik scale of the map.
-           index_position (str): None or 'side' (index on side),
-              'bottom' (index at bottom).
-
-        Returns a list of tuples (paper name, width in mm, height in
-        mm, portrait_ok, landscape_ok, is_default). Paper sizes are
-        represented in portrait mode.
-        """
-
-        # the mapnik scale depends on the latitude
-        lat = bounding_box.get_top_left()[0]
-        scale *= math.cos(math.radians(lat))
-
-        # by convention, mapnik uses 90 ppi whereas cairo uses 72 ppi
-        scale *= float(72) / 90
-
-        geo_height_m, geo_width_m = bounding_box.spheric_sizes()
-        paper_width_mm = geo_width_m * 1000 / scale
-        paper_height_mm = geo_height_m * 1000 / scale
-
-        LOG.debug('Map represents %dx%dm, needs at least %.1fx%.1fcm '
-                  'on paper.' % (geo_width_m, geo_height_m,
-                                 paper_width_mm/10., paper_height_mm/10.))
-
-        # Take index into account, when applicable
-        if index_position == 'side':
-            paper_width_mm /= (1. -
-                               SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO)
-        elif index_position == 'bottom':
-            paper_height_mm /= (1. -
-                                SinglePageRenderer.MAX_INDEX_OCCUPATION_RATIO)
-
-        # Take margins into account
-        paper_width_mm += 2 * 
commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT)
-        paper_height_mm += 2 * 
commons.convert_pt_to_mm(Renderer.PRINT_SAFE_MARGIN_PT)
-
-        # Take grid legend, title and copyright into account
-        paper_width_mm /= 1 - Renderer.GRID_LEGEND_MARGIN_RATIO
-        paper_height_mm /= 1 - (Renderer.GRID_LEGEND_MARGIN_RATIO + 0.05 + 
0.02)
-
-        # Transform the values into integers
-        paper_width_mm  = int(math.ceil(paper_width_mm))
-        paper_height_mm = int(math.ceil(paper_height_mm))
-
-        LOG.debug('Best fit is %.1fx%.1fcm.' % (paper_width_mm/10., 
paper_height_mm/10.))
-
-        # Test both portrait and landscape orientations when checking for paper
-        # sizes.
-        valid_sizes = []
-        for name, w, h in ocitysmap2.layoutlib.PAPER_SIZES:
-            portrait_ok  = paper_width_mm <= w and paper_height_mm <= h
-            landscape_ok = paper_width_mm <= h and paper_height_mm <= w
-
-            if portrait_ok or landscape_ok:
-                valid_sizes.append([name, w, h, portrait_ok, landscape_ok, 
False])
-
-        # Add a 'Custom' paper format to the list that perfectly matches the
-        # bounding box.
-        valid_sizes.append(['Best fit',
-                            min(paper_width_mm, paper_height_mm),
-                            max(paper_width_mm, paper_height_mm),
-                            paper_width_mm < paper_height_mm,
-                            paper_width_mm > paper_height_mm,
-                            False])
-
-        # select the first one as default
-        valid_sizes[0][5] = True
-
-        return valid_sizes
-
-
-class SinglePageRendererNoIndex(SinglePageRenderer):
-
-    name = 'plain'
-    description = 'Full-page layout without index.'
-
-    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
-        """
-        Create the renderer.
-
-        Args:
-           rc (RenderingConfiguration): rendering parameters.
-           tmpdir (os.path): Path to a temp dir that can hold temp files.
-        """
-        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
None)
-
-
-    @staticmethod
-    def get_compatible_paper_sizes(bounding_box,
-                                   scale=Renderer.DEFAULT_SCALE):
-        """Returns a list of the compatible paper sizes for the given bounding
-        box. The list is sorted, smaller papers first, and a "custom" paper
-        matching the dimensions of the bounding box is added at the end.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map geographic bounding box.
-            scale (int): minimum mapnik scale of the map.
-
-        Returns a list of tuples (paper name, width in mm, height in
-        mm, portrait_ok, landscape_ok). Paper sizes are represented in
-        portrait mode.
-        """
-        return SinglePageRenderer._generic_get_compatible_paper_sizes(
-            bounding_box, scale, None)
-
-
-class SinglePageRendererIndexOnSide(SinglePageRenderer):
-
-    name = 'single_page_index_side'
-    description = 'Full-page layout with the index on the side.'
-
-    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
-        """
-        Create the renderer.
-
-        Args:
-           rc (RenderingConfiguration): rendering parameters.
-           tmpdir (os.path): Path to a temp dir that can hold temp files.
-        """
-        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
'side')
-
-    @staticmethod
-    def get_compatible_paper_sizes(bounding_box,
-                                   scale=Renderer.DEFAULT_SCALE):
-        """Returns a list of the compatible paper sizes for the given bounding
-        box. The list is sorted, smaller papers first, and a "custom" paper
-        matching the dimensions of the bounding box is added at the end.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map geographic bounding box.
-            scale (int): minimum mapnik scale of the map.
-
-        Returns a list of tuples (paper name, width in mm, height in
-        mm, portrait_ok, landscape_ok). Paper sizes are represented in
-        portrait mode.
-        """
-        return SinglePageRenderer._generic_get_compatible_paper_sizes(
-            bounding_box, scale, 'side')
-
-
-class SinglePageRendererIndexBottom(SinglePageRenderer):
-
-    name = 'single_page_index_bottom'
-    description = 'Full-page layout with the index at the bottom.'
-
-    def __init__(self, db, rc, tmpdir, dpi, file_prefix):
-        """
-        Create the renderer.
-
-        Args:
-           rc (RenderingConfiguration): rendering parameters.
-           tmpdir (os.path): Path to a temp dir that can hold temp files.
-        """
-        SinglePageRenderer.__init__(self, db, rc, tmpdir, dpi, file_prefix, 
'bottom')
-
-    @staticmethod
-    def get_compatible_paper_sizes(bounding_box,
-                                   scale=Renderer.DEFAULT_SCALE):
-        """Returns a list of the compatible paper sizes for the given bounding
-        box. The list is sorted, smaller papers first, and a "custom" paper
-        matching the dimensions of the bounding box is added at the end.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map geographic bounding box.
-            scale (int): minimum mapnik scale of the map.
-
-        Returns a list of tuples (paper name, width in mm, height in
-        mm, portrait_ok, landscape_ok). Paper sizes are represented in
-        portrait mode.
-        """
-        return SinglePageRenderer._generic_get_compatible_paper_sizes(
-            bounding_box, scale, 'bottom')
-
-
-if __name__ == '__main__':
-    import renderers
-    import coords
-    from ocitysmap2 import i18n
-
-    # Hack to fake gettext
-    try:
-        _(u"Test gettext")
-    except NameError:
-        __builtins__.__dict__["_"] = lambda x: x
-
-    logging.basicConfig(level=logging.DEBUG)
-
-    bbox = coords.BoundingBox(48.8162, 2.3417, 48.8063, 2.3699)
-    zoom = 16
-
-    renderer_cls = renderers.get_renderer_class_by_name('plain')
-    papers = renderer_cls.get_compatible_paper_sizes(bbox, zoom)
-
-    print 'Compatible paper sizes:'
-    for p in papers:
-        print '  * %s (%.1fx%.1fcm)' % (p[0], p[1]/10.0, p[2]/10.0)
-    print 'Using first available:', papers[0]
-
-    class StylesheetMock:
-        def __init__(self):
-            # self.path = '/home/sam/src/python/maposmatic/mapnik-osm/osm.xml'
-            self.path = 
'/mnt/data1/common/home/d2/Downloads/svn/mapnik-osm/osm.xml'
-            self.grid_line_color = 'black'
-            self.grid_line_alpha = 0.9
-            self.grid_line_width = 2
-            self.shade_color = 'black'
-            self.shade_alpha = 0.7
-
-    class RenderingConfigurationMock:
-        def __init__(self):
-            self.stylesheet = StylesheetMock()
-            self.bounding_box = bbox
-            self.paper_width_mm = papers[0][1]
-            self.paper_height_mm = papers[0][2]
-            self.i18n  = i18n.i18n()
-            self.title = 'Au Kremlin-Bycêtre'
-            self.polygon_wkt = bbox.as_wkt()
-
-    config = RenderingConfigurationMock()
-
-    plain = renderer_cls(config, '/tmp', None)
-    surface = cairo.PDFSurface('/tmp/plain.pdf',
-                   commons.convert_mm_to_pt(config.paper_width_mm),
-                   commons.convert_mm_to_pt(config.paper_height_mm))
-
-    plain.render(surface, commons.PT_PER_INCH)
-    surface.finish()
-
-    print "Generated /tmp/plain.pdf"
diff --git a/ocitysmap2/maplib/__init__.py b/ocitysmap2/maplib/__init__.py
deleted file mode 100644
index dad0e7e..0000000
--- a/ocitysmap2/maplib/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
diff --git a/ocitysmap2/maplib/grid.py b/ocitysmap2/maplib/grid.py
deleted file mode 100644
index 435830b..0000000
--- a/ocitysmap2/maplib/grid.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import logging
-import math
-
-import shapes
-
-l = logging.getLogger('ocitysmap')
-
-class Grid:
-    """
-    The Grid class defines the grid overlayed on a rendered map. It controls
-    the grid size, number and size of squares, etc.
-    """
-
-    # Approximative paper size of the grid squares (+/- 33%).
-    GRID_APPROX_PAPER_SIZE_MM = 40
-
-    def __init__(self, bounding_box, scale, rtl=False):
-        """Creates a new grid for the given bounding box.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map bounding box.
-            rtl (boolean): whether the map is rendered in right-to-left mode or
-                not. Defaults to False.
-        """
-
-        self._bbox = bounding_box
-        self.rtl   = rtl
-        self._height_m, self._width_m = bounding_box.spheric_sizes()
-
-        l.info('Laying out grid on %.1fx%.1fm area...' %
-               (self._width_m, self._height_m))
-
-        # compute the terrain grid size corresponding to the targeted paper 
size
-        size = float(self.GRID_APPROX_PAPER_SIZE_MM) * scale / 1000
-        # compute the scientific notation of this size :
-        # size = significant * 10 ^ exponent with 1 <= significand < 10
-        exponent = math.log10(size)
-        significand = float(size) / 10 ** int(exponent)
-        # "round" this size to be 1, 2, 2.5 or 5 multiplied by a power of 10
-        if significand < 1.5:
-            significand = 1
-        elif significand < 2.25:
-            significand = 2
-        elif significand < 3.75:
-            significand = 2.5
-        elif significand < 7.5:
-            significand = 5
-        else:
-            significand = 10
-        size = significand * 10 ** int(exponent)
-        # use it
-        self.grid_size_m = size
-        self.horiz_count = self._width_m / size
-        self.vert_count = self._height_m / size
-
-        self._horiz_angle_span = abs(self._bbox.get_top_left()[1] -
-                                     self._bbox.get_bottom_right()[1])
-        self._vert_angle_span  = abs(self._bbox.get_top_left()[0] -
-                                     self._bbox.get_bottom_right()[0])
-
-        self._horiz_unit_angle = (self._horiz_angle_span / self.horiz_count)
-        self._vert_unit_angle  = (self._vert_angle_span / self.vert_count)
-
-        self._horizontal_lines = [ ( self._bbox.get_top_left()[0] -
-                                    (x+1) * self._vert_unit_angle)
-                                  for x in 
xrange(int(math.floor(self.vert_count)))]
-        self._vertical_lines   = [ (self._bbox.get_top_left()[1] +
-                                    (x+1) * self._horiz_unit_angle)
-                                   for x in 
xrange(int(math.floor(self.horiz_count)))]
-
-        self.horizontal_labels = map(self._gen_horizontal_square_label,
-                                      xrange(int(math.ceil(self.horiz_count))))
-        self.vertical_labels = map(self._gen_vertical_square_label,
-                                   xrange(int(math.ceil(self.vert_count))))
-
-        l.info('Using %sx%sm grid (%sx%s squares).' %
-               (self.grid_size_m, self.grid_size_m,
-                self.horiz_count, self.vert_count))
-
-    def generate_shape_file(self, filename):
-        """Generates the grid shapefile with all the horizontal and
-        vertical lines added.
-
-        Args:
-            filename (string): path to the temporary shape file that will be
-                generated.
-        Returns the ShapeFile object.
-        """
-
-        # Use a slightly larger bounding box for the shape file to accomodate
-        # for the small imprecisions of re-projecting.
-        g = shapes.LineShapeFile(self._bbox.create_expanded(0.001, 0.001),
-                                 filename, 'grid')
-        map(g.add_vert_line, self._vertical_lines)
-        map(g.add_horiz_line, self._horizontal_lines)
-        return g
-
-    def _gen_horizontal_square_label(self, x):
-        """Generates a human-readable label for the given horizontal square
-        number. For example:
-             1 -> A
-             2 -> B
-            26 -> Z
-            27 -> AA
-            28 -> AB
-            ...
-        """
-        if self.rtl:
-            x = len(self._vertical_lines) - x
-
-        label = ''
-        while x != -1:
-            label = chr(ord('A') + x % 26) + label
-            x = x/26 - 1
-        return label
-
-    def _gen_vertical_square_label(self, x):
-        """Generate a human-readable label for the given vertical square
-        number. Since we put numbers verticaly, this is simply x+1."""
-        return str(x + 1)
-
-    def get_location_str(self, lattitude, longitude):
-        """
-        Translate the given lattitude/longitude (EPSG:4326) into a
-        string of the form "CA42"
-        """
-        hdelta = min(abs(longitude - self._bbox.get_top_left()[1]),
-                     self._horiz_angle_span)
-        hlabel = self.horizontal_labels[int(hdelta / self._horiz_unit_angle)]
-
-        vdelta = min(abs(lattitude - self._bbox.get_top_left()[0]),
-                     self._vert_angle_span)
-        vlabel = self.vertical_labels[int(vdelta / self._vert_unit_angle)]
-
-        return "%s%s" % (hlabel, vlabel)
-
-
-if __name__ == "__main__":
-    from ocitysmap2 import coords
-
-    logging.basicConfig(level=logging.DEBUG)
-    grid = Grid(coords.BoundingBox(44.4883, -1.0901, 44.4778, -1.0637))
-    shape = grid.generate_shape_file('/tmp/mygrid.shp')
diff --git a/ocitysmap2/maplib/map_canvas.py b/ocitysmap2/maplib/map_canvas.py
deleted file mode 100644
index 0db3db4..0000000
--- a/ocitysmap2/maplib/map_canvas.py
+++ /dev/null
@@ -1,229 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import logging
-
-# Importing mapnik2 raises a DeprecationWarning as of mapnik
-# commit 14700dba. As mapnik 2.1 (or git version with support for
-# placement-type="simple") is required for OCitySMap (see INSTALL),
-# instead of importing mapnik2, we import mapnik and assert it isn't
-# an old version.
-import mapnik
-assert mapnik.mapnik_version >= 200100, \
-    "Mapnik module version %s is too old, see ocitysmap's INSTALL " \
-    "for more details." % mapnik.mapnik_version_string()
-
-import os
-
-from ocitysmap2 import coords
-from layoutlib.commons import convert_pt_to_dots
-import shapes
-import math
-
-l = logging.getLogger('ocitysmap')
-
-_MAPNIK_PROJECTION = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 " \
-                     "+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m   " \
-                     "address@hidden +no_defs +over"
-
-class MapCanvas:
-    """
-    The MapCanvas renders a geographic bounding box into a Cairo surface of a
-    given width and height (in pixels). Shape files can be overlayed on the
-    map; the order they are added to the map being important with regard to
-    their respective alpha levels.
-    """
-
-    def __init__(self, stylesheet, bounding_box, _width, _height, dpi,
-                 extend_bbox_to_ratio=True):
-        """Initialize the map canvas for rendering.
-
-        Args:
-            stylesheet (Stylesheet): map stylesheet.
-            bounding_box (coords.BoundingBox): geographic bounding box.
-            graphical_ratio (float): ratio of the map area (width/height).
-            extend_bbox_to_ratio (boolean): allow MapCanvas to extend
-            the bounding box to make it match the ratio of the
-            provided rendering area. Needed by SinglePageRenderer.
-        """
-
-        self._proj = mapnik.Projection(_MAPNIK_PROJECTION)
-
-        # This is where the magic of the map canvas happens. Given an original
-        # bounding box and a graphical ratio for the output, the bounding box
-        # is adjusted (extended) to fill the destination zone. See
-        # _fix_bbox_ratio for more details on how this is done.
-        orig_envelope = self._project_envelope(bounding_box)
-        graphical_ratio = _width / _height
-
-        if extend_bbox_to_ratio:
-            off_x, off_y, width, height = self._fix_bbox_ratio(
-                orig_envelope.minx, orig_envelope.miny,
-                orig_envelope.width(), orig_envelope.height(),
-                graphical_ratio)
-
-            envelope = mapnik.Box2d(off_x, off_y, off_x+width, off_y+height)
-            self._geo_bbox = self._inverse_envelope(envelope)
-            l.debug('Corrected bounding box from %s to %s, ratio: %.2f.' %
-                    (bounding_box, self._geo_bbox, graphical_ratio))
-        else:
-            envelope = orig_envelope
-            self._geo_bbox = bounding_box
-
-        g_width  = int(convert_pt_to_dots(_width, dpi))
-        g_height = int(convert_pt_to_dots(_height, dpi))
-
-        # Create the Mapnik map with the corrected width and height and zoom to
-        # the corrected bounding box ('envelope' in the Mapnik jargon)
-        self._map = mapnik.Map(g_width, g_height, _MAPNIK_PROJECTION)
-        mapnik.load_map(self._map, stylesheet.path)
-        self._map.zoom_to_box(envelope)
-
-        # Added shapes to render
-        self._shapes = []
-
-        l.info('MapCanvas rendering map on %dx%dpx.' % (g_width, g_height))
-
-    def _fix_bbox_ratio(self, off_x, off_y, width, height, dest_ratio):
-        """Adjusts the area expressed by its origin's offset and its size to
-        the given destination ratio by tweaking one of the two dimensions
-        depending on the current ratio and the destination ratio."""
-        cur_ratio = float(width)/height
-
-        if cur_ratio < dest_ratio:
-            w = width
-            width *= float(dest_ratio)/cur_ratio
-            off_x -= (width - w)/2.0
-        else:
-            h = height
-            height *= float(cur_ratio)/dest_ratio
-            off_y -= (height - h)/2.0
-
-        return map(int, (off_x, off_y, width, height))
-
-    def add_shape_file(self, shape_file, str_color='grey', alpha=0.5,
-                       line_width=1.0):
-        """
-        Args:
-            shape_file (shapes.ShapeFile): path to the shape file to overlay on
-                this map canvas.
-            str_color (string): litteral name of the layer's color, needs to be
-                understood by mapnik.Color.
-            alpha (float): transparency factor in the range 0 (invisible) -> 1
-                (opaque).
-            line_width (float): line width for the features that will be drawn.
-        """
-        col = mapnik.Color(str_color)
-        col.a = int(255 * alpha)
-        self._shapes.append({'shape_file': shape_file,
-                             'color': col,
-                             'line_width': line_width})
-        l.debug('Added shape file %s to map canvas as layer %s.' %
-                (shape_file.get_filepath(), shape_file.get_layer_name()))
-
-    def render(self):
-        """Render the map in memory with all the added shapes. The Mapnik Map
-        object can be accessed with self.get_rendered_map()."""
-
-        # Add all shapes to the map
-        for shape in self._shapes:
-            self._render_shape_file(**shape)
-
-    def get_rendered_map(self):
-        return self._map
-
-    def get_actual_bounding_box(self):
-        """Returns the actual geographic bounding box that will be rendered by
-        Mapnik."""
-        return self._geo_bbox
-
-    def get_actual_scale(self):
-        # get the scale denominator computed by mapnik
-        scale = self._map.scale_denominator()
-        # the actual scale depends on the latitude
-        lat = self._geo_bbox.get_top_left()[0]
-        scale *= math.cos(math.radians(lat))
-        # by convention, the scale denominator uses 90 ppi whereas cairo uses 
72 ppi
-        scale *= float(72) / 90
-        return scale
-
-    def _render_shape_file(self, shape_file, color, line_width):
-        shape_file.flush()
-
-        shpid = os.path.basename(shape_file.get_filepath())
-        s,r = mapnik.Style(), mapnik.Rule()
-        r.symbols.append(mapnik.PolygonSymbolizer(color))
-        r.symbols.append(mapnik.LineSymbolizer(color, line_width))
-        s.rules.append(r)
-
-        self._map.append_style('style_%s' % shpid, s)
-        layer = mapnik.Layer(shpid)
-        layer.datasource = mapnik.Shapefile(file=shape_file.get_filepath())
-        layer.styles.append('style_%s' % shpid)
-
-        self._map.layers.append(layer)
-
-    def _project_envelope(self, bbox):
-        """Project the given bounding box into the rendering projection."""
-        envelope = mapnik.Box2d(bbox.get_top_left()[1],
-                                bbox.get_top_left()[0],
-                                bbox.get_bottom_right()[1],
-                                bbox.get_bottom_right()[0])
-        c0 = self._proj.forward(mapnik.Coord(envelope.minx, envelope.miny))
-        c1 = self._proj.forward(mapnik.Coord(envelope.maxx, envelope.maxy))
-        return mapnik.Box2d(c0.x, c0.y, c1.x, c1.y)
-
-    def _inverse_envelope(self, envelope):
-        """Inverse the given cartesian envelope (in 900913) back to a 4002
-        bounding box."""
-        c0 = self._proj.inverse(mapnik.Coord(envelope.minx, envelope.miny))
-        c1 = self._proj.inverse(mapnik.Coord(envelope.maxx, envelope.maxy))
-        return coords.BoundingBox(c0.y, c0.x, c1.y, c1.x)
-
-if __name__ == '__main__':
-    logging.basicConfig(level=logging.DEBUG)
-
-    class StylesheetMock:
-        def __init__(self):
-            self.path = '/home/sam/src/python/maposmatic/mapnik-osm/osm.xml'
-
-    bbox = coords.BoundingBox(48.7148, 2.0155, 48.6950, 2.0670)
-    canvas = MapCanvas(StylesheetMock(), bbox, 297.0/210)
-    new_bbox = canvas.get_actual_bounding_box()
-
-    canvas.add_shape_file(
-        shapes.LineShapeFile(new_bbox, '/tmp/mygrid.shp', 'grid')
-            .add_vert_line(2.04)
-            .add_horiz_line(48.7),
-        'red', 0.3, 10.0)
-
-    canvas.add_shape_file(
-        shapes.PolyShapeFile(new_bbox, '/tmp/mypoly.shp', 'shade')
-            .add_shade_from_wkt('POLYGON((2.04537559754772 
48.702794853359,2.0456929723376 48.7033682610593,2.0457757970068 
48.7037022715908,2.04577876144723 48.7043963708738,2.04589724923321 
48.7043963708738,2.04589428479277 48.704519562418,2.04746445007788 
48.7044706533954,2.04723043894637 48.7024665875529,2.04674876229103 
48.7024238422904,2.04615641319268 48.702500973452,2.04537559754772 
48.702794853359))'),
-        'blue', 0.3)
-
-    canvas.render()
-    mapnik.render_to_file(canvas.get_rendered_map(), '/tmp/mymap.png', 'png')
-
-    print "Generated /tmp/mymap.png"
diff --git a/ocitysmap2/maplib/overview_grid.py 
b/ocitysmap2/maplib/overview_grid.py
deleted file mode 100644
index eb1bcea..0000000
--- a/ocitysmap2/maplib/overview_grid.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-
-# Copyright (C) 2012  Étienne Loks
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-
-import logging
-import math
-
-import shapes
-
-l = logging.getLogger('ocitysmap')
-
-class OverviewGrid:
-    """
-    The OverviewGrid class draw the grid overlayed on the overview map of a
-    multi-page render.
-    """
-
-    def __init__(self, bounding_box, pages_bounding_boxes, rtl=False):
-        """Creates a new grid for the given bounding boxes.
-
-        Args:
-            bounding_box (coords.BoundingBox): the map bounding box.
-            bounding_box (list of coords.BoundingBox): bounding boxes of the
-                pages.
-            rtl (boolean): whether the map is rendered in right-to-left mode or
-                not. Defaults to False.
-        """
-
-        self._bbox = bounding_box
-        self._pages_bbox = pages_bounding_boxes
-        self.rtl   = rtl
-        self._height_m, self._width_m = bounding_box.spheric_sizes()
-
-        l.info('Laying out of overview grid on %.1fx%.1fm area...' %
-               (self._width_m, self._height_m))
-
-    def generate_shape_file(self, filename):
-        """Generates the grid shapefile with all the horizontal and
-        vertical lines added.
-
-        Args:
-            filename (string): path to the temporary shape file that will be
-                generated.
-        Returns the ShapeFile object.
-        """
-
-        # Use a slightly larger bounding box for the shape file to accomodate
-        # for the small imprecisions of re-projecting.
-        g = shapes.BoxShapeFile(self._bbox.create_expanded(0.001, 0.001),
-                                 filename, 'grid')
-        map(g.add_box, self._pages_bbox)
-        return g
-
-
-
-if __name__ == "__main__":
-    logging.basicConfig(level=logging.DEBUG)
-    pass
diff --git a/ocitysmap2/maplib/shapes.py b/ocitysmap2/maplib/shapes.py
deleted file mode 100644
index fe915fd..0000000
--- a/ocitysmap2/maplib/shapes.py
+++ /dev/null
@@ -1,192 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# ocitysmap, city map and street index generator from OpenStreetMap data
-# Copyright (C) 2010  David Decotigny
-# Copyright (C) 2010  Frédéric Lehobey
-# Copyright (C) 2010  Pierre Mauduit
-# Copyright (C) 2010  David Mentré
-# Copyright (C) 2010  Maxime Petazzoni
-# Copyright (C) 2010  Thomas Petazzoni
-# Copyright (C) 2010  Gaël Utard
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import locale
-import logging
-import os
-
-# The ogr module is now known as osgeo.ogr in recent versions of the
-# module, but we want to keep compatibility with older versions
-try:
-    from osgeo import ogr
-except ImportError:
-    import ogr
-
-l = logging.getLogger('ocitysmap')
-
-class _ShapeFile:
-    """
-    This class represents a shapefile (.shp) that can be added to a Mapnik map
-    as a layer. It provides a few methods to add some geometry 'features' to
-    the shape file.
-
-    This is a private base class and is not meant to be used directly from the
-    outside.
-    """
-
-    def __init__(self, bounding_box, out_filename, layer_name):
-        """
-        Args:
-            bounding_box (BoundingBox): bounding box of the map area.
-            out_filename (string): path to the output shape file to generate.
-            layer_name (string): layer name for the shape file.
-        """
-
-        self._bbox = bounding_box
-        self._filepath = out_filename
-        self._layer_name = layer_name
-
-        driver = ogr.GetDriverByName('ESRI Shapefile')
-        if os.path.exists(out_filename):
-            # Delete the detination file first
-            driver.DeleteDataSource(out_filename)
-
-        self._ds = driver.CreateDataSource(out_filename)
-        self._layer = None
-
-    def _add_feature(self, feature):
-        f = ogr.Feature(feature_def=self._layer.GetLayerDefn())
-        f.SetGeometryDirectly(feature)
-        self._layer.CreateFeature(f)
-        f.Destroy()
-
-    def flush(self):
-        """
-        Commit the file to disk and prevent any further addition of
-        new longitude/latitude lines
-        """
-        self._ds.Destroy()
-        self._ds = None
-
-    def get_layer_name(self):
-        """Returns the name of the layer used for this shape file."""
-        return self._layer_name
-
-    def get_filepath(self):
-        """Returns the path to the destination shape file."""
-        return self._filepath
-
-    def __str__(self):
-        return "ShapeFile(%s)" % self._filepath
-
-class LineShapeFile(_ShapeFile):
-    """
-    Shape file for LineString geometries.
-    """
-
-    def __init__(self, bounding_box, out_filename, layer_name):
-        _ShapeFile.__init__(self, bounding_box, out_filename, layer_name)
-        self._layer = self._ds.CreateLayer(self._layer_name,
-                                           geom_type=ogr.wkbLineString)
-        l.debug('Created layer %s in LineShapeFile %s.' %
-                (layer_name, out_filename))
-
-    def add_bounding_rectangle(self):
-        self.add_horiz_line(self._bbox.get_top_left()[0])
-        self.add_horiz_line(self._bbox.get_bottom_right()[0])
-        self.add_vert_line(self._bbox.get_top_left()[1])
-        self.add_vert_line(self._bbox.get_bottom_right()[1])
-        return self
-
-    def add_horiz_line(self, y):
-        """Add a new latitude line at the given latitude."""
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(self._bbox.get_top_left()[1], y)
-        line.AddPoint_2D(self._bbox.get_bottom_right()[1], y)
-        self._add_feature(line)
-        return self
-
-    def add_vert_line(self, x):
-        """Add a new longitude line at the given longitude."""
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(x, self._bbox.get_top_left()[0])
-        line.AddPoint_2D(x, self._bbox.get_bottom_right()[0])
-        self._add_feature(line)
-        return self
-
-class BoxShapeFile(LineShapeFile):
-    """
-    Shape file for Box geometries.
-    """
-
-    def add_box(self, box):
-        top_left, bottom_right = box.get_top_left(), box.get_bottom_right()
-
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(*list(reversed(top_left)))
-        line.AddPoint_2D(bottom_right[1], top_left[0])
-        self._add_feature(line)
-
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(bottom_right[1], top_left[0])
-        line.AddPoint_2D(*list(reversed(bottom_right)))
-        self._add_feature(line)
-
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(*list(reversed(bottom_right)))
-        line.AddPoint_2D(top_left[1], bottom_right[0])
-        self._add_feature(line)
-
-        line = ogr.Geometry(type = ogr.wkbLineString)
-        line.AddPoint_2D(top_left[1], bottom_right[0])
-        line.AddPoint_2D(*list(reversed(top_left)))
-        self._add_feature(line)
-        return self
-
-class PolyShapeFile(_ShapeFile):
-    """
-    Shape file for Polygon geometries.
-    """
-
-    def __init__(self, bounding_box, out_filename, layer_name):
-        _ShapeFile.__init__(self, bounding_box, out_filename, layer_name)
-        self._layer = self._ds.CreateLayer(self._layer_name,
-                                           geom_type=ogr.wkbPolygon)
-        l.debug('Created layer %s in PolyShapeFile %s.' %
-                (layer_name, out_filename))
-
-    def add_shade_from_wkt(self, wkt):
-        """Add the polygon feature to the shape file."""
-        # Prevent the current locale from influencing how the WKT data is
-        # parsed by OGR.
-        prev_locale = locale.getlocale(locale.LC_ALL)
-        locale.setlocale(locale.LC_ALL, "C")
-
-        try:
-            poly = ogr.CreateGeometryFromWkt(wkt)
-        finally:
-            locale.setlocale(locale.LC_ALL, prev_locale)
-
-        self._add_feature(poly)
-        return self
-
-if __name__ == "__main__":
-    from ocitysmap2 import coords
-
-    logging.basicConfig(level=logging.DEBUG)
-    (LineShapeFile(coords.BoundingBox(44.4883, -1.0901, 44.4778, -1.0637),
-                   '/tmp/mygrid.shp', 'test')
-        .add_horiz_line(44.48)
-        .add_vert_line(-1.08)
-        .flush())
diff --git a/render.py b/render.py
new file mode 100755
index 0000000..4f681ea
--- /dev/null
+++ b/render.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python
+# -*- coding: utf-8; mode: Python -*-
+
+# ocitysmap, city map and street index generator from OpenStreetMap data
+# Copyright (C) 2009  David Decotigny
+# Copyright (C) 2009  Frédéric Lehobey
+# Copyright (C) 2009  David Mentré
+# Copyright (C) 2009  Maxime Petazzoni
+# Copyright (C) 2009  Thomas Petazzoni
+# Copyright (C) 2009  Gaël Utard
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+__version__ = '0.22'
+
+import logging
+import optparse
+import os
+import sys
+
+import ocitysmap
+import ocitysmap.layoutlib.renderers
+from coords import BoundingBox
+
+def main():
+    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
+
+    # Paper sizes, sorted in increasing widths
+    KNOWN_PAPER_SIZE_NAMES = \
+        map(lambda p: p[0],
+            sorted(ocitysmap.layoutlib.PAPER_SIZES,
+                   key=lambda p: p[1]))
+
+    # Known renderer names
+    KNOWN_RENDERERS_NAMES = \
+        map(lambda r: "%s (%s)" % (r.name, r.description),
+            ocitysmap.layoutlib.renderers.get_renderers())
+
+    # Known paper orientations
+    KNOWN_PAPER_ORIENTATIONS = ['portrait', 'landscape']
+
+    usage = '%prog [options] [-b <lat1,long1 lat2,long2>|--osmid <osmid>]'
+    parser = optparse.OptionParser(usage=usage,
+                                   version='%%prog %s' % __version__)
+    parser.add_option('-C', '--config', dest='config_file', metavar='FILE',
+                      help='specify the location of the config file.')
+    parser.add_option('-p', '--prefix', dest='output_prefix', metavar='PREFIX',
+                      help='set a prefix to the generated file names. '
+                           'Defaults to "citymap".',
+                      default='citymap')
+    parser.add_option('-f', '--format', dest='output_formats', metavar='FMT',
+                      help='specify the output formats. Supported file '
+                           'formats: svg, svgz, pdf, ps, ps.gz, png, and csv. '
+                           'Defaults to PDF. May be specified multiple times.',
+                      action='append')
+    parser.add_option('-t', '--title', dest='output_title', metavar='TITLE',
+                      help='specify the title displayed in the output files.',
+                      default="My Map")
+    parser.add_option('--osmid', dest='osmid', metavar='OSMID',
+                      help='OSM ID representing the polygon of the city '
+                      'to render.', type="int"),
+    parser.add_option('-b', '--bounding-box', dest='bbox',  nargs=2,
+                      metavar='LAT1,LON1 LAT2,LON2',
+                      help='bounding box (EPSG: 4326).')
+    parser.add_option('-L', '--language', dest='language',
+                      metavar='LANGUAGE_CODE',
+                      help='language to use when generating the index '
+                           '(default=fr_FR.UTF-8). The map language is '
+                           'driven by the system\' locale setting.',
+                      default='fr_FR.UTF-8')
+    parser.add_option('-s', '--stylesheet', dest='stylesheet',
+                      metavar='NAME',
+                      help='specify which stylesheet to use. Defaults to the '
+                      'first specified in the configuration file.')
+    parser.add_option('-l', '--layout', dest='layout',
+                      metavar='NAME',
+                      default=KNOWN_RENDERERS_NAMES[0].split()[0],
+                      help=('specify which layout to use. Available layouts '
+                            'are: %s. Defaults to %s.' %
+                            (', '.join(KNOWN_RENDERERS_NAMES),
+                             KNOWN_RENDERERS_NAMES[0].split()[0])))
+    parser.add_option('--paper-format', metavar='FMT',
+                      help='set the output paper format. Either "default", '
+                           'or one of %s.' % ', '.join(KNOWN_PAPER_SIZE_NAMES),
+                      default='default')
+    parser.add_option('--orientation', metavar='ORIENTATION',
+                      help='set the output paper orientation. Either '
+                            '"portrait" or "landscape". Defaults to portrait.',
+                      default='portrait')
+
+    (options, args) = parser.parse_args()
+    if len(args):
+        parser.print_help()
+        return 1
+
+    # Make sure either -b or -c is given
+    optcnt = 0
+    for var in options.bbox, options.osmid:
+        if var:
+            optcnt += 1
+
+    if optcnt == 0:
+        parser.error("One of --bounding-box "
+                     "or --osmid is mandatory")
+
+    if optcnt > 1:
+        parser.error("Options --bounding-box "
+                     "or --osmid are exclusive")
+
+    # Parse config file and instanciate main object
+    mapper = ocitysmap.OCitySMap(
+        [options.config_file or os.path.join(os.environ["HOME"], 
'.ocitysmap.conf')])
+
+    # Parse bounding box arguments when given
+    bbox = None
+    if options.bbox:
+        try:
+            bbox = BoundingBox.parse_latlon_strtuple(options.bbox)
+        except ValueError:
+            parser.error('Invalid bounding box!')
+        # Check that latitude and langitude are different
+        lat1, lon1 = bbox.get_top_left()
+        lat2, lon2 = bbox.get_bottom_right()
+        if lat1 == lat2:
+            parser.error('Same latitude in bounding box corners')
+        if lon1 == lon2:
+            parser.error('Same longitude in bounding box corners')
+
+    # Parse OSM id when given
+    if options.osmid:
+        try:
+            bbox  = BoundingBox.parse_wkt(
+                mapper.get_geographic_info(options.osmid)[0])
+        except LookupError:
+            parser.error('No such OSM id: %d' % options.osmid)
+
+    # Parse stylesheet (defaults to 1st one)
+    if options.stylesheet is None:
+        stylesheet = mapper.get_all_style_configurations()[0]
+    else:
+        try:
+            stylesheet = mapper.get_stylesheet_by_name(options.stylesheet)
+        except LookupError, ex:
+            parser.error("%s. Available stylesheets: %s."
+                 % (ex, ', '.join(map(lambda s: s.name,
+                      mapper.STYLESHEET_REGISTRY))))
+
+    # Parse rendering layout
+    if options.layout is None:
+        cls_renderer = ocitysmap.layoutlib.renderers.get_renderers()[0]
+    else:
+        try:
+            cls_renderer = 
ocitysmap.layoutlib.renderers.get_renderer_class_by_name(options.layout)
+        except LookupError, ex:
+            parser.error("%s\nAvailable layouts: %s."
+                 % (ex, ', '.join(map(lambda lo: "%s (%s)"
+                          % (lo.name, lo.description),
+                          ocitysmap.layoutlib.renderers.get_renderers()))))
+
+    # Output file formats
+    if not options.output_formats:
+        options.output_formats = ['pdf']
+    options.output_formats = set(options.output_formats)
+
+    # Reject output formats that are not supported by the renderer
+    compatible_output_formats = cls_renderer.get_compatible_output_formats()
+    for format in options.output_formats:
+        if format not in compatible_output_formats:
+            parser.error("Output format %s not supported by layout %s" %
+                         (format, cls_renderer.name))
+
+    # Parse paper size
+    if (options.paper_format != 'default') \
+            and options.paper_format not in KNOWN_PAPER_SIZE_NAMES:
+        parser.error("Invalid paper format. Allowed formats = default, %s"
+                     % ', '.join(KNOWN_PAPER_SIZE_NAMES))
+
+    # Determine actual paper size
+    compat_papers = cls_renderer.get_compatible_paper_sizes(bbox)
+    if not compat_papers:
+        parser.error("No paper size compatible with this rendering.")
+
+    paper_descr = None
+    if options.paper_format == 'default':
+        for p in compat_papers:
+            if p[5]:
+                paper_descr = p
+                break
+    else:
+        # Make sure the requested paper size is in list
+        for p in compat_papers:
+            if p[0] == options.paper_format:
+                paper_descr = p
+                break
+    if not paper_descr:
+        parser.error("Requested paper format not compatible with rendering. 
Compatible paper formats are: %s."
+             % ', '.join(map(lambda p: "%s (%.1fx%.1fcm²)"
+                % (p[0], p[1]/10., p[2]/10.),
+                compat_papers)))
+    assert paper_descr[3] or paper_descr[4] # Portrait or Landscape accepted
+
+    # Validate requested orientation
+    if options.orientation not in KNOWN_PAPER_ORIENTATIONS:
+        parser.error("Invalid paper orientation. Allowed orientations: %s"
+                     % KNOWN_PAPER_ORIENTATIONS)
+
+    if (options.orientation == 'portrait' and not paper_descr[3]) or \
+        (options.orientation == 'landscape' and not paper_descr[4]):
+        parser.error("Requested paper orientation %s not compatible with this 
rendering at this paper size." % options.orientation)
+
+    # Prepare the rendering config
+    rc              = ocitysmap.RenderingConfiguration()
+    rc.title        = options.output_title
+    rc.osmid        = options.osmid or None # Force to None if absent
+    rc.bounding_box = bbox
+    rc.language     = options.language
+    rc.stylesheet   = stylesheet
+    if options.orientation == 'portrait':
+        rc.paper_width_mm  = paper_descr[1]
+        rc.paper_height_mm = paper_descr[2]
+    else:
+        rc.paper_width_mm  = paper_descr[2]
+        rc.paper_height_mm = paper_descr[1]
+
+    # Go !...
+    mapper.render(rc, cls_renderer.name, options.output_formats,
+                  options.output_prefix)
+
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/setup.py b/setup.py
index 3d8bb3e..7585321 100755
--- a/setup.py
+++ b/setup.py
@@ -39,11 +39,13 @@ designed to be printed.
       license="GPL",
       maintainer="The Hackfest2009 team",
       maintainer_email="address@hidden",
-      packages = ['ocitysmap2',
-                  'ocitysmap2.maplib',
-                  'ocitysmap2.indexlib',
-                  'ocitysmap2.layoutlib' ],
-      scripts = ['ocitysmap2-render' ],
-      data_files = [ ('share/images/ocitysmap2',
-                      ['images/osm-logo.png', 'images/osm-logo.svg']) ]
+      packages = ['ocitysmap',
+                  'ocitysmap.maplib',
+                  'ocitysmap.indexlib',
+                  'ocitysmap.layoutlib' ],
+      scripts = ['render.py' ],
+      data_files = [
+          ('share/images/ocitysmap', ['images/osm-logo.png',
+                                      'images/osm-logo.svg'])
+      ]
 )
diff --git a/support/test-suite.sh b/support/test-suite.sh
index b1a3686..81a7262 100644
--- a/support/test-suite.sh
+++ b/support/test-suite.sh
@@ -21,47 +21,47 @@ TESTS=(
 TESTID=0
 
 for tst in address@hidden ; do
-    type=$(echo $tst | cut -f1 -d':')
-    title=$(echo $tst | cut -f2 -d':')
-    ref=$(echo $tst | cut -f3 -d':')
-    renderer=$(echo $tst | cut -f4 -d':')
-    paper_format=$(echo $tst | cut -f5 -d':')
-    paper_orientation=$(echo $tst | cut -f6 -d':')
+  type=$(echo $tst | cut -f1 -d':')
+  title=$(echo $tst | cut -f2 -d':')
+  ref=$(echo $tst | cut -f3 -d':')
+  renderer=$(echo $tst | cut -f4 -d':')
+  paper_format=$(echo $tst | cut -f5 -d':')
+  paper_orientation=$(echo $tst | cut -f6 -d':')
 
-    if [ $type == "osmid" ] ; then
-       area_opt="--osmid=$ref"
-    else
-       bbox_part1=$(echo $ref|cut -f1 -d'-')
-       bbox_part2=$(echo $ref|cut -f2 -d'-')
-       area_opt="-b ${bbox_part1} ${bbox_part2}"
-    fi
+  if [ $type == "osmid" ] ; then
+    area_opt="--osmid=$ref"
+  else
+    bbox_part1=$(echo $ref|cut -f1 -d'-')
+    bbox_part2=$(echo $ref|cut -f2 -d'-')
+    area_opt="-b ${bbox_part1} ${bbox_part2}"
+  fi
 
-    if [ $renderer == "multi_page" ] ; then
-       output_formats="-f pdf"
-    else
-       output_formats="-f png -f pdf -f svgz"
-    fi
+  if [ $renderer == "multi_page" ] ; then
+    output_formats="-f pdf"
+  else
+    output_formats="-f png -f pdf -f svgz"
+  fi
 
-    printf "\e[31m>>> Starting test with\n area='%s'\n renderer='%s'\n 
formats='%s'\n paper='%s'\n orientation='%s'\n title='%s'\n\n\e[m" \
-       "$area_opt" \
-       "$renderer" \
-       "$output_formats" \
-       "$paper_format" \
-       "$paper_orientation" \
-       "$title"
+  printf "\e[31m>>> Starting test with\n area='%s'\n renderer='%s'\n 
formats='%s'\n paper='%s'\n orientation='%s'\n title='%s'\n\n\e[m" \
+    "$area_opt" \
+    "$renderer" \
+    "$output_formats" \
+    "$paper_format" \
+    "$paper_orientation" \
+    "$title"
 
-    ./ocitysmap2-render \
-       $output_formats \
-       -l $renderer \
-       $area_opt \
-       -p test_$TESTID \
-       -t "$title" \
-       --paper-format=$paper_format \
-       --orientation=$paper_orientation
+  ./render.py \
+    $output_formats \
+    -l $renderer \
+    $area_opt \
+    -p test_$TESTID \
+    -t "$title" \
+    --paper-format=$paper_format \
+    --orientation=$paper_orientation
 
-    if [ $? -ne 0 ] ; then
-       echo "==== ERROR, ABORTING"
-       exit 1
-    fi
-    TESTID=$((TESTID+1))
+  if [ $? -ne 0 ] ; then
+    echo "==== ERROR, ABORTING"
+    exit 1
+  fi
+  TESTID=$((TESTID+1))
 done
-- 
1.7.10




reply via email to

[Prev in Thread] Current Thread [Next in Thread]