diff --git a/src/api/urls.py b/src/api/urls.py
index fc95fb5b8b303f617498d1b6f7153f53325efe07..011ccc3f9bbc44b8ad2fcc8bcf309f94169a1992 100644
--- a/src/api/urls.py
+++ b/src/api/urls.py
@@ -32,6 +32,7 @@ urlpatterns = [
     path('c/<slug:conference>/assembly/<slug:assembly>/issue_redeem_token/<str:issuing_token>',
          badges.BadgeTokenListCreate.as_view(), name='badge-token-create-with-token'),
     path('c/<slug:conference>/assembly/<slug:assembly>/badges/<uuid:pk>', badges.BadgeTokenListCreate.as_view(), name='badge-token-list-create'),
+    path('c/<slug:conference>/map/poi.json', maps.PoiExportView.as_view(), name='map-poi'),
     path('c/<slug:conference>/map/assemblies/poi.json', maps.AssembliesPoiExportView.as_view(),  name='map-assemblies-poi'),
     path('c/<slug:conference>/map/assemblies/areas.json', maps.AssembliesAreasExportView.as_view(), name='map-assemblies-areas'),
     path('c/<slug:conference>/badges/redeem_token', badges.redeem_badge_token, name='badge-redeem'),
diff --git a/src/api/views/maps.py b/src/api/views/maps.py
index 078157d6717a50683f424f47f1dda667474c61e6..2129db883a89c0a68106e4b2e62cae36132105a2 100644
--- a/src/api/views/maps.py
+++ b/src/api/views/maps.py
@@ -5,29 +5,72 @@ from django.contrib.gis.gdal import SpatialReference, CoordTransform
 from rest_framework.views import APIView
 
 from core.models.assemblies import Assembly
+from core.models.map import MapPOI
 from core.models.conference import ConferenceExportCache
 from .mixins import ConferenceSlugMixin
 
 
+_cts = {}  # cache of CoordTransforms (if needed)
+
+
+def get_field_geojson(value, srid: int, ct_cache: dict = None):
+    if value.srid != srid:
+        if ct_cache is None:
+            ct_cache = {}
+        if value.srid not in ct_cache:
+            srs = SpatialReference(srid)
+            ct_cache[value.srid] = CoordTransform(value.srs, srs)
+        value.transform(ct_cache[value.srid])
+    return json.loads(value.geojson)
+
+
+class PoiExportView(ConferenceSlugMixin, APIView):
+    def get_geojson(self):
+        srid = 4326  # RFC7946 mandates WGS84
+        ct_cache = {}
+        features = []
+        result = {
+            'type': 'FeatureCollection',
+            # 'crs': {'type': 'name', 'properties': {'name': f'EPSG:{srid}'}},  # deprecated, not in RFC7946
+            'features': features,
+        }
+
+        for poi in MapPOI.objects.filter(conference=self.conference, visible=True).exclude(location_point=None):
+            feature = {
+                'type': 'Feature',
+                'id': str(poi.id),
+                'geometry': get_field_geojson(poi.location_point, srid=srid, ct_cache=ct_cache),
+                'properties': {
+                    'layer': poi.get_location_layer_index(),
+                    'type': 'poi',
+                    'name': poi.name,
+                    'official': poi.is_official,
+                },
+            }
+            features.append(feature)
+
+        return result
+
+    def get(self, request, *args, **kwargs):
+        cache_id = 'geojson-poi'
+
+        return ConferenceExportCache.handle_http_request(
+            request=request,
+            conference=self.conference,
+            type=ConferenceExportCache.Type.MAP,
+            ident=cache_id,
+            content_type='application/geo+json',
+            result_func=lambda: json.dumps(self.get_geojson()),
+        )
+
+
 class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta):
     geometry_field = None
-    _cts = {}  # cache of CoordTransforms (if needed)
 
     def get_queryset(self):
         exportable_states = Assembly.PLACED_STATES + [Assembly.State.HIDDEN]
         return Assembly.objects.filter(conference=self.conference, state_assembly__in=exportable_states)
 
-    @staticmethod
-    def get_field_geojson(value, srid: int, ct_cache: dict = None):
-        if value.srid != srid:
-            if ct_cache is None:
-                ct_cache = {}
-            if value.srid not in ct_cache:
-                srs = SpatialReference(srid)
-                ct_cache[value.srid] = CoordTransform(value.srs, srs)
-            value.transform(ct_cache[value.srid])
-        return json.loads(value.geojson)
-
     def get_geometry_field(self, obj):
         return getattr(obj, self.geometry_field)
 
@@ -49,8 +92,10 @@ class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta):
             feature = {
                 'type': 'Feature',
                 'id': assembly.slug,
-                'geometry': self.get_field_geojson(geometry, srid=srid, ct_cache=ct_cache),
+                'geometry': get_field_geojson(geometry, srid=srid, ct_cache=ct_cache),
                 'properties': {
+                    'layer': assembly.get_location_layer_index(),
+                    'type': 'assembly',
                     'name': assembly.name,
                     'official': assembly.is_official,
                     'cluster': assembly.is_cluster,
diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po
index aaca57d4f704d45a971e367a7d1eb3842d23e38b..a3875c1c295e85a1c53a891cf77d6bd9214c52c6 100644
--- a/src/backoffice/locale/de/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/de/LC_MESSAGES/django.po
@@ -747,6 +747,9 @@ msgstr "Vouchers"
 msgid "nav_channels"
 msgstr "Channels-Team"
 
+msgid "nav_map"
+msgstr "Karte"
+
 msgid "nav_users"
 msgstr "Teilnehmer"
 
@@ -819,6 +822,38 @@ msgstr "Noch keinen Account?"
 msgid "registration_password_reset_link"
 msgstr "Passwort vergessen?"
 
+# use translation from core
+msgid "MapLayer"
+msgstr ""
+
+# use translation from core
+msgid "MapLayers"
+msgstr ""
+
+# use translation from core
+msgid "MapLayer__index"
+msgstr ""
+
+# use translation from core
+msgid "MapLayer__name"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI"
+msgstr ""
+
+# use translation from core
+msgid "MapPOIs"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI__visible"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI__name"
+msgstr ""
+
 msgid "Project-new"
 msgstr "Neues Projekt"
 
@@ -1392,6 +1427,9 @@ msgstr "Selforganized Session aktualisiert"
 msgid "Event--sos-removed%s"
 msgstr "Selforganized Session %s gelöscht"
 
+msgid "all"
+msgstr "alle"
+
 msgid "backoffice:assembly-organisational-data"
 msgstr "Organisatorisches"
 
diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po
index fe780fb909aac8eb81969642e7e7a4c14f418585..2a6575fb016d6f02887dc1978dff01757d639857 100644
--- a/src/backoffice/locale/en/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/en/LC_MESSAGES/django.po
@@ -746,6 +746,9 @@ msgstr "Vouchers"
 msgid "nav_channels"
 msgstr "channels team"
 
+msgid "nav_map"
+msgstr "map"
+
 msgid "nav_users"
 msgstr "users"
 
@@ -818,6 +821,38 @@ msgstr "No account yet?"
 msgid "registration_password_reset_link"
 msgstr "Forgot password?"
 
+# use translation from core
+msgid "MapLayer"
+msgstr ""
+
+# use translation from core
+msgid "MapLayers"
+msgstr ""
+
+# use translation from core
+msgid "MapLayer__index"
+msgstr ""
+
+# use translation from core
+msgid "MapLayer__name"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI"
+msgstr ""
+
+# use translation from core
+msgid "MapPOIs"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI__visible"
+msgstr ""
+
+# use translation from core
+msgid "MapPOI__name"
+msgstr ""
+
 msgid "Project-new"
 msgstr "new project"
 
@@ -1390,6 +1425,9 @@ msgstr "selforganized session updated"
 msgid "Event--sos-removed%s"
 msgstr "selforganized session %s deleted"
 
+msgid "all"
+msgstr "all"
+
 msgid "backoffice:assembly-organisational-data"
 msgstr "Organisational Data"
 
diff --git a/src/backoffice/templates/backoffice/base.html b/src/backoffice/templates/backoffice/base.html
index 81dd727f1c838157ff72333a4c756d57d2a28226..d078c94d4657f7f4c2b78243b5335563b1f7b8d6 100644
--- a/src/backoffice/templates/backoffice/base.html
+++ b/src/backoffice/templates/backoffice/base.html
@@ -44,6 +44,11 @@
             <a class="nav-link{% if active_page == 'channels' %} active{% endif %}" href="{% url 'backoffice:channels' %}">{% trans "nav_channels" %}{% if active_page == 'channels' %} <span class="visually-hidden">{{ activetab_srmarker }}</span>{% endif %}</a>
           </li>
           {% endif %}
+          {% if has_map %}
+          <li class="nav-item">
+            <a class="nav-link{% if active_page == 'map' %} active{% endif %}" href="{% url 'backoffice:map-poi-list' %}">{% trans "nav_map" %}{% if active_page == 'map' %} <span class="visually-hidden">{{ activetab_srmarker }}</span>{% endif %}</a>
+          </li>
+          {% endif %}
           {% if has_users %}
           <li class="nav-item">
             <a class="nav-link{% if active_page == 'users' %} active{% endif %}" href="{% url 'backoffice:users' %}">{% trans "nav_users" %}{% if active_page == 'users' %} <span class="visually-hidden">{{ activetab_srmarker }}</span>{% endif %}</a>
diff --git a/src/backoffice/templates/backoffice/map_layer_form.html b/src/backoffice/templates/backoffice/map_layer_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..c84d6aa07f4e1528de2323474bb8e03afa4f1fd6
--- /dev/null
+++ b/src/backoffice/templates/backoffice/map_layer_form.html
@@ -0,0 +1,41 @@
+{% extends 'backoffice/base.html' %}
+{% load django_bootstrap5 %}
+{% load i18n %}
+
+{% block title %}
+{{ object.name }}
+{% endblock %}
+
+{% block content %}
+<div class="row mb-3">
+  <div class="col-md-12">
+    <form action="" method="POST" enctype="multipart/form-data">{% csrf_token %}
+    <div class="card border-default">
+      <div class="card-header bg-default">
+        {% if object.id %}
+            <span class="text-muted float-end text-end me-2" style="font-size: 50%;">ID: <strong>{{ object.id }}</strong></span>
+        {% trans 'MapLayer' %} "{{ object.name }}"
+        {% else %}
+            {% trans 'MapLayer' %} <i class="bi bi-plus-circle"></i>
+        {% endif %}
+      </div>
+      <div class="card-body">
+          {% bootstrap_field form.index %}
+          <div class="row">
+              <div class="col-md-6">
+                  {% bootstrap_field form.name_de %}
+              </div>
+              <div class="col-md-6">
+                  {% bootstrap_field form.name_en %}
+              </div>
+          </div>
+      </div>
+      <div class="card-body">
+        <button type="submit" class="btn btn-primary">{% trans "save" %}</button>
+      </div>
+    </div>
+    </form>
+  </div>
+</div>
+
+{% endblock %}
diff --git a/src/backoffice/templates/backoffice/map_layer_list.html b/src/backoffice/templates/backoffice/map_layer_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..d7e98e79c4edfe3850286d65914d21de73e09682
--- /dev/null
+++ b/src/backoffice/templates/backoffice/map_layer_list.html
@@ -0,0 +1,73 @@
+{% extends 'backoffice/base.html' %}
+{% load humanize %}
+{% load i18n %}
+{% load static %}
+
+{% block htmlhead %}
+    <link rel="stylesheet" href="{% static 'datatables/datatables.min.css' %}">
+{% endblock %}
+
+{% block scripts %}
+    <script src="{% static 'datatables/datatables.min.js' %}"></script>
+    <script>
+      $(document).ready(function() {
+          $('#pois').DataTable({
+            pageLength: 100,
+            language: {
+              "decimal":        "",
+                "emptyTable":     "{% trans 'assembly_info_no_data' %}",
+                "info":           "{% trans 'assembly_info_paginated' %}",
+                "infoEmpty":      "{% trans 'assembly_info_empty' %}",
+                "infoFiltered":   "{% trans 'assembly_info_filtered' %}",
+                "infoPostFix":    "",
+                "thousands":      " ",
+                "lengthMenu":     "{% trans 'assembly_paginate_menu' %}",
+                "loadingRecords": "LOADING ...",
+                "processing":     "Processing...",
+                "search":         "{% trans 'assembly_search' %}",
+                "zeroRecords":    "{% trans 'assembly_noentries' %}",
+                "paginate": {
+                    "first":      "{% trans 'assembly_paginate_first' %}",
+                    "last":       "{% trans 'assembly_paginate_last' %}",
+                    "next":       "{% trans 'assembly_paginate_next' %}",
+                    "previous":   "{% trans 'assembly_paginate_previous' %}"
+                },
+                "aria": {
+                    "sortAscending":  ": activate to sort column ascending",
+                    "sortDescending": ": activate to sort column descending"
+                }
+            }
+          });
+      });
+    </script>
+{% endblock %}
+
+{% block content %}
+
+<div class="card">
+  <div class="card-header">
+    <a href="{% url 'backoffice:map-layer-create' %}" class="float-end btn btn-sm btn-primary"><i class="bi bi-plus-circle"></i> {% trans 'add' %}</a>
+      {% trans 'MapLayers' %}
+  </div>
+  <div class="card-body">
+
+    <table class="table table-sm" id="pois">
+      <thead>
+        <tr>
+          <th>{% trans "MapLayer__index" %}</th>
+          <th>{% trans "MapLayer__name" %}</th>
+        </tr>
+      </thead>
+      <tbody>
+  {% for layer in object_list %}
+        <tr>
+          <td>{{ layer.index }}</td>
+          <td><a href="{% url 'backoffice:map-layer-edit' pk=layer.pk %}">{{ layer.name }}</a></td>
+        </tr>
+  {% endfor %}
+      </tbody>
+    </table>
+
+  </div>
+</div>
+{% endblock %}
diff --git a/src/backoffice/templates/backoffice/map_poi_form.html b/src/backoffice/templates/backoffice/map_poi_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..4c71ac78da23e4c85105b6da16221dd68253642c
--- /dev/null
+++ b/src/backoffice/templates/backoffice/map_poi_form.html
@@ -0,0 +1,55 @@
+{% extends 'backoffice/base.html' %}
+{% load django_bootstrap5 %}
+{% load i18n %}
+
+{% block title %}
+{{ object.name }}
+{% endblock %}
+
+{% block content %}
+<div class="row mb-3">
+  <div class="col-md-12">
+    <form action="" method="POST" enctype="multipart/form-data">{% csrf_token %}
+    <div class="card border-default">
+      <div class="card-header bg-default">
+        {% if object.id %}
+            <span class="text-muted float-end text-end me-2" style="font-size: 50%;">ID: <strong>{{ object.id }}</strong></span>
+        {% trans 'MapPOI' %} "{{ object.name }}"
+        {% else %}
+            {% trans 'MapPOI' %} <i class="bi bi-plus-circle"></i>
+        {% endif %}
+      </div>
+      <div class="card-body">
+          {% bootstrap_field form.visible %}
+          {% bootstrap_field form.is_official %}
+          <div class="row">
+              <div class="col-md-6">
+                  {% bootstrap_field form.name_de %}
+              </div>
+              <div class="col-md-6">
+                  {% bootstrap_field form.name_en %}
+              </div>
+          </div>
+          <div class="row">
+              <div class="col-md-6">
+                  {% bootstrap_field form.description_de %}
+              </div>
+              <div class="col-md-6">
+                  {% bootstrap_field form.description_en %}
+              </div>
+          </div>
+
+          {% bootstrap_field form.location_layer %}
+          {% include "core/map.html" with map_config=conference.map_config.backoffice poi_id="poi" areas_id="areas" %}
+          <input type="hidden" name="location_point" id="poi" value="{{ poi.get_location_point_as_json }}">
+
+      </div>
+      <div class="card-body">
+        <button type="submit" class="btn btn-primary">{% trans "save" %}</button>
+      </div>
+    </div>
+    </form>
+  </div>
+</div>
+
+{% endblock %}
diff --git a/src/backoffice/templates/backoffice/map_poi_list.html b/src/backoffice/templates/backoffice/map_poi_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..8c2841966577b8c319a7174b081f280ee2a52918
--- /dev/null
+++ b/src/backoffice/templates/backoffice/map_poi_list.html
@@ -0,0 +1,73 @@
+{% extends 'backoffice/base.html' %}
+{% load humanize %}
+{% load i18n %}
+{% load static %}
+
+{% block htmlhead %}
+    <link rel="stylesheet" href="{% static 'datatables/datatables.min.css' %}">
+{% endblock %}
+
+{% block scripts %}
+    <script src="{% static 'datatables/datatables.min.js' %}"></script>
+    <script>
+      $(document).ready(function() {
+          $('#pois').DataTable({
+            pageLength: 100,
+            language: {
+              "decimal":        "",
+                "emptyTable":     "{% trans 'assembly_info_no_data' %}",
+                "info":           "{% trans 'assembly_info_paginated' %}",
+                "infoEmpty":      "{% trans 'assembly_info_empty' %}",
+                "infoFiltered":   "{% trans 'assembly_info_filtered' %}",
+                "infoPostFix":    "",
+                "thousands":      " ",
+                "lengthMenu":     "{% trans 'assembly_paginate_menu' %}",
+                "loadingRecords": "LOADING ...",
+                "processing":     "Processing...",
+                "search":         "{% trans 'assembly_search' %}",
+                "zeroRecords":    "{% trans 'assembly_noentries' %}",
+                "paginate": {
+                    "first":      "{% trans 'assembly_paginate_first' %}",
+                    "last":       "{% trans 'assembly_paginate_last' %}",
+                    "next":       "{% trans 'assembly_paginate_next' %}",
+                    "previous":   "{% trans 'assembly_paginate_previous' %}"
+                },
+                "aria": {
+                    "sortAscending":  ": activate to sort column ascending",
+                    "sortDescending": ": activate to sort column descending"
+                }
+            }
+          });
+      });
+    </script>
+{% endblock %}
+
+{% block content %}
+
+<div class="card">
+  <div class="card-header">
+    <a href="{% url 'backoffice:map-poi-create' %}" class="float-end btn btn-sm btn-primary"><i class="bi bi-plus-circle"></i> {% trans 'add' %}</a>
+      {% trans 'MapPOIs' %}
+  </div>
+  <div class="card-body">
+
+    <table class="table table-sm" id="pois">
+      <thead>
+        <tr>
+          <th>{% trans "MapPOI__visible" %}</th>
+          <th>{% trans "MapPOI__name" %}</th>
+        </tr>
+      </thead>
+      <tbody>
+  {% for poi in object_list %}
+        <tr class="{% if not poi.visible %}text-muted{% endif %}">
+          <td>{{ poi.visible|yesno }}</td>
+          <td><a href="{% url 'backoffice:map-poi-edit' pk=poi.pk %}">{{ poi.name }}</a></td>
+        </tr>
+  {% endfor %}
+      </tbody>
+    </table>
+
+  </div>
+</div>
+{% endblock %}
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index ce2b6ef2c4e4560c71fbb3348df5a980a6f35c39..6ec50ab99444ba33c337d5c4387917f8173ab337 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -8,6 +8,7 @@ from .views import \
     auth, \
     channelteam, \
     events, \
+    map, \
     misc, \
     pages, \
     profile, \
@@ -97,6 +98,13 @@ urlpatterns = [
     path('assembly/<uuid:assembly>/r/<uuid:room>/remove_link', assemblies.RemoveRoomLinkView.as_view(), name='roomlink-remove'),
     path('assembly/<uuid:assembly>/r/<uuid:room>/remove', assemblies.RemoveRoomView.as_view(), name='assembly-remove-room'),
 
+    path('map/layers', map.LayerListView.as_view(), name='map-layer-list'),
+    path('map/layer/new', map.LayerCreateView.as_view(), name='map-layer-create'),
+    path('map/layer/<uuid:pk>', map.LayerUpdateView.as_view(), name='map-layer-edit'),
+    path('map/pois', map.POIListView.as_view(), name='map-poi-list'),
+    path('map/poi/new', map.POICreateView.as_view(), name='map-poi-create'),
+    path('map/poi/<uuid:pk>', map.POIUpdateView.as_view(), name='map-poi-edit'),
+
     path('schedule/', schedules.SchedulesIndexView.as_view(), name='schedules'),
     path('schedule/sources', schedules.ScheduleSourcesListView.as_view(), name='schedulesource-list'),
     path('schedule/source/add', schedules.ScheduleSourcesCreateView.as_view(), name='schedulesource-add'),
diff --git a/src/backoffice/views/map.py b/src/backoffice/views/map.py
new file mode 100644
index 0000000000000000000000000000000000000000..85026ee725b2b4c953b78d9072534c59f910f5de
--- /dev/null
+++ b/src/backoffice/views/map.py
@@ -0,0 +1,137 @@
+import logging
+
+from django.contrib import messages
+from django.urls import reverse
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import ListView
+from django.views.generic.detail import SingleObjectTemplateResponseMixin
+from django.views.generic.edit import UpdateView, CreateView
+
+from core.models.map import MapPOI, MapLayer
+from .mixins import ConferenceMixin, guess_active_sidebar_item
+
+logger = logging.getLogger(__name__)
+
+
+class MapAdminMixin(ConferenceMixin):
+    require_conference = True
+    permission_required = ['core.map_edit']
+
+    def get_context_data(self, *args, **kwargs):
+        ctx = super().get_context_data(*args, **kwargs)
+        ctx['active_page'] = 'map'
+
+        layers = [{
+            'caption': f'{layer["name"]} ({layer["index"]})',
+            'link': reverse('backoffice:map-layer-edit', kwargs={'pk': layer["pk"]}),
+        } for layer in MapLayer.objects.filter(conference=self.conference).order_by('index').values('pk', 'name', 'index')]
+
+        pois = []
+        poi_count = 0
+        ctx['sidebar'] = {
+            'title': _('nav_map'),
+            # 'title_link': reverse(self.base_view_name),
+            'items': [
+                {
+                    'caption': _('MapLayers'),
+                    'children': layers,
+                    'count': len(layers),
+                    'add_link': reverse('backoffice:map-layer-create'),
+                },
+                {
+                    'caption': _('MapPOIs'),
+                    'children': pois,
+                    'add_link': reverse('backoffice:map-poi-create'),
+                },
+            ],
+        }
+
+        for poi in MapPOI.objects.filter(conference=self.conference).values('id', 'visible', 'name').iterator():
+            poi_count += 1
+            pois.append({
+                'caption': poi['name'] if poi['visible'] else format_html('<s>{}</s>', poi['name']),
+                'link': reverse('backoffice:map-poi-edit', kwargs={'pk': poi['id']}),
+            })
+        pois.insert(0, {
+            'caption': format_html('<i>({all})</i>', all=_('all')),
+            'link': reverse('backoffice:map-poi-list'),
+            'count': poi_count,
+        })
+
+        # try to guess 'active' sidebar item
+        guess_active_sidebar_item(self.request, ctx['sidebar']['items'], with_query_string=False)
+
+        return ctx
+
+
+class LayerListView(MapAdminMixin, ListView):
+    template_name = 'backoffice/map_layer_list.html'
+
+    model = MapLayer
+
+    def get_queryset(self, *args, **kwargs):
+        return MapLayer.objects.filter(conference=self.conference).order_by('index')
+
+
+class LayerFormMixin(MapAdminMixin, SingleObjectTemplateResponseMixin):
+    fields = ['name_de', 'name_en', 'index']
+    template_name = 'backoffice/map_layer_form.html'
+    model = MapLayer
+
+    def get_queryset(self, *args, **kwargs):
+        return MapLayer.objects.filter(conference=self.conference)
+
+    def form_valid(self, form):
+        messages.success(self.request, _('updated'))
+        # TODO: ConferenceExportCache.signal_poi_modification(self.conference)
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return reverse('backoffice:map-poi-edit', kwargs={'pk': self.object.id})
+
+
+class LayerCreateView(LayerFormMixin, CreateView):
+    def form_valid(self, form):
+        form.instance.conference = self.conference
+        return super().form_valid(form)
+
+
+class LayerUpdateView(LayerFormMixin, UpdateView):
+    pass
+
+
+class POIListView(MapAdminMixin, ListView):
+    template_name = 'backoffice/map_poi_list.html'
+
+    model = MapPOI
+
+    def get_queryset(self, *args, **kwargs):
+        return MapPOI.objects.filter(conference=self.conference).order_by('name')
+
+
+class POIFormMixin(MapAdminMixin, SingleObjectTemplateResponseMixin):
+    model = MapPOI
+    fields = ['visible', 'is_official', 'name_de', 'name_en', 'description_de', 'description_en', 'location_layer', 'location_point']
+    template_name = 'backoffice/map_poi_form.html'
+
+    def get_queryset(self, *args, **kwargs):
+        return MapPOI.objects.filter(conference=self.conference)
+
+    def form_valid(self, form):
+        messages.success(self.request, _('updated'))
+        # TODO: ConferenceExportCache.signal_poi_modification(self.conference)
+        return super().form_valid(form)
+
+    def get_success_url(self):
+        return reverse('backoffice:map-poi-edit', kwargs={'pk': self.object.id})
+
+
+class POICreateView(POIFormMixin, CreateView):
+    def form_valid(self, form):
+        form.instance.conference = self.conference
+        return super().form_valid(form)
+
+
+class POIUpdateView(POIFormMixin, UpdateView):
+    pass
diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py
index a1f95d32da8e39c35cf43c28b745b5cf978502fe..fed9d0c48cf34196513875ab816d925b1558200a 100644
--- a/src/backoffice/views/mixins.py
+++ b/src/backoffice/views/mixins.py
@@ -100,6 +100,7 @@ class ConferenceMixin(LoginRequiredMixin, PermissionRequiredMixin):
                 'has_assemblies': self.is_assembly_team,
                 'has_channel': self.is_channel_team,
                 'has_pages': self.request.user.has_conference_staffpermission(self.conference, 'core.static_pages'),
+                'has_map': self.request.user.has_conference_staffpermission(self.conference, 'core.map_edit'),
                 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.platformusers'),
                 'has_schedules': self.request.user.has_conference_staffpermission(self.conference, 'core.scheduleadmin'),
                 'has_workadventure':
@@ -110,6 +111,7 @@ class ConferenceMixin(LoginRequiredMixin, PermissionRequiredMixin):
                 'has_assemblies': False,
                 'has_channel': False,
                 'has_pages': False,
+                'has_map': False,
                 'has_users': False,
                 'has_schedules': False,
                 'has_workadventure': False,
diff --git a/src/core/admin.py b/src/core/admin.py
index 1ae94c86bda2afcbc444f9b7f8bb5846b03ceb9b..faf5d55b5523d3a9bf697e8fb33d406dc5a39b8c 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -13,6 +13,7 @@ from .models import \
     PlatformUser, \
     Room, RoomLink, \
     Assembly, AssemblyLink, AssemblyMember, AssemblyLogEntry, \
+    MapLayer, MapPOI, \
     Badge, BadgeToken, BadgeTokenTimeConstraint, \
     ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping, \
     StaticPage, StaticPageRevision, \
@@ -302,6 +303,47 @@ class AssemblyLogEntryAdmin(admin.ModelAdmin):
     ]
 
 
+class MapLayerAdmin(admin.ModelAdmin):
+    list_display = ['conference', 'index', 'name']
+    list_display_links = ['name']
+    list_filter = ['conference']
+    readonly_fields = ['id', 'conference']
+
+    def get_readonly_fields(self, request, obj=None, **kwargs):
+        result = list(super().get_readonly_fields(request, obj, **kwargs))
+        if obj is None:
+            result.remove('conference')
+        return result
+
+
+class MapPOIAdmin(GISModelAdmin):
+    list_display = ['conference', 'visible', 'name', 'is_official']
+    list_display_links = ['name']
+    list_filter = ['conference', 'visible', 'is_official']
+    readonly_fields = ['id', 'conference']
+    search_fields = ['name', 'description']
+
+    fieldsets = (
+        ('Organisation', {
+            'fields': ['id', 'conference'],
+        }),
+        ('Data', {
+            'fields': ['visible', 'is_official',
+                       ('name_de', 'name_en'),
+                       ('description_de', 'description_en')],
+        }),
+        ('Location', {
+            'fields': ['location_layer', 'location_point'],
+        }),
+    )
+
+    def get_readonly_fields(self, request, obj=None, **kwargs):
+        result = list(super().get_readonly_fields(request, obj, **kwargs))
+        if obj is None:
+            result.remove('conference')
+        return result
+
+
 class BadgeTokenTimeConstraintInline(admin.TabularInline):
     model = BadgeTokenTimeConstraint
     fields = ['date_time_range']
@@ -645,6 +687,8 @@ admin.site.register(Assembly, AssemblyAdmin)
 admin.site.register(AssemblyLogEntry, AssemblyLogEntryAdmin)
 admin.site.register(BadgeToken, BadgeTokenAdmin)
 admin.site.register(Event, EventAdmin)
+admin.site.register(MapLayer, MapLayerAdmin)
+admin.site.register(MapPOI, MapPOIAdmin)
 admin.site.register(Room, RoomAdmin)
 admin.site.register(ScheduleSource, ScheduleSourceAdmin)
 admin.site.register(ScheduleSourceImport, ScheduleSourceImportAdmin)
diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index 7cf8882a13c15c946df320dfe0782cccdb5efe14..869b48aa342329ec1d7e71ab559dc2f056afbb09 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -243,6 +243,12 @@ msgstr "öffentlich sichtbare E-Mail für Kontakt"
 msgid "Assembly__public_contact"
 msgstr "öffentlicher Kontakt"
 
+msgid "Assembly__location_layer__help"
+msgstr "Ebene, auf der sich die Assembly befindet"
+
+msgid "Assembly__location_layer"
+msgstr "Ebene"
+
 msgid "Assembly__location_point__help"
 msgstr "Position der Assembly für z. B. die Such-Funktion, hier sollte also der \"Haupteingang\" gewählt werden"
 
@@ -511,6 +517,9 @@ msgstr "Channel-Team: alle Assemblies verwaltbar, auch noch nicht fertig angeleg
 msgid "ConferenceMember__permission-static_pages"
 msgstr "Statische Seiten: Verwaltung von Info-Seiten"
 
+msgid "ConferenceMember__permission-map_edit"
+msgstr "Bearbeitung der Karte (POIs)"
+
 msgid "ConferenceMember__permission-block_platformuser"
 msgstr "Nutzer blockieren"
 
@@ -943,6 +952,69 @@ msgstr "persönlicher Kommentar"
 msgid "EventParticipant__must_be_conference_member"
 msgstr "Dieser Nutzer ist kein Teilnehmer der Konferenz."
 
+msgid "MapLayer"
+msgstr "Ebene"
+
+msgid "MapLayers"
+msgstr "Ebenen"
+
+msgid "MapLayer__index__help"
+msgstr "numerischer Index des Layer, wird u.a. beim Tile-Zugriff benutzt"
+
+msgid "MapLayer__index"
+msgstr "index"
+
+msgid "MapLayer__name__help"
+msgstr "sprechender Name"
+
+msgid "MapLayer__name"
+msgstr "Name"
+
+msgid "MapPOI"
+msgstr "POI"
+
+msgid "MapPOIs"
+msgstr "POIs"
+
+msgid "MapPOI__visible__help"
+msgstr "wird dieser POI auf der Karte dargestellt?"
+
+msgid "MapPOI__visible"
+msgstr "sichtbar"
+
+msgid "MapPOI__name__help"
+msgstr "sprechender Name des Point Of Interest"
+
+msgid "MapPOI__name"
+msgstr "Name"
+
+msgid "MapPOI__description__help"
+msgstr "öffentliche Information"
+
+msgid "MapPOI__description"
+msgstr "Beschreibung"
+
+msgid "MapPOI__is_official__help"
+msgstr "Dies ist ein für die Veranstaltung relevanter Point-Of-Interest (z. B. Kasse, Info-Desk)."
+
+msgid "MapPOI__is_official"
+msgstr "offiziell"
+
+msgid "MapPOI__location_layer__help"
+msgstr "auf welcher Ebene befindet sich der POI"
+
+msgid "MapPOI__location_layer"
+msgstr "Ebene"
+
+msgid "MapPOI__location_point__help"
+msgstr "tatsächliche Position des POI auf der Karte"
+
+msgid "MapPOI__location_point"
+msgstr "Position"
+
+msgid "MapPOI__need-both-layer-and-point"
+msgstr "Es werden sowohl Ebene als auch Position benötigt, eines alleine ist nicht aussagekräftig."
+
 msgid "DirectMessage__sender__help"
 msgstr "Absender der Nachricht (leer = System)"
 
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index 5ff8090eda68f186b284c6f474747121f2317855..76badc5e69a0be0c10338451e2e09929543b0640 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -243,6 +243,12 @@ msgstr "publicly visible email for contact"
 msgid "Assembly__public_contact"
 msgstr "public contact"
 
+msgid "Assembly__location_layer__help"
+msgstr "layer, on which this assembly is placed"
+
+msgid "Assembly__location_layer"
+msgstr "layer"
+
 msgid "Assembly__location_point__help"
 msgstr "point on the map where this location (assembly) can be found, consider choosing its \"main entrance\" here"
 
@@ -511,6 +517,9 @@ msgstr "channel team: manage all assemblies, see also incomplete and rejected re
 msgid "ConferenceMember__permission-static_pages"
 msgstr "static pages: manage information pages"
 
+msgid "ConferenceMember__permission-map_edit"
+msgstr "edit the map (POIs)"
+
 msgid "ConferenceMember__permission-block_platformuser"
 msgstr "block conference members"
 
@@ -943,6 +952,69 @@ msgstr "personal comment"
 msgid "EventParticipant__must_be_conference_member"
 msgstr "The participant must be a member of the conference."
 
+msgid "MapLayer"
+msgstr "layer"
+
+msgid "MapLayers"
+msgstr "layers"
+
+msgid "MapLayer__index__help"
+msgstr "numerical ordering (or: identifier) of this map layer"
+
+msgid "MapLayer__index"
+msgstr "index"
+
+msgid "MapLayer__name__help"
+msgstr "descriptive name of this layer"
+
+msgid "MapLayer__name"
+msgstr "name"
+
+msgid "MapPOI"
+msgstr "POI"
+
+msgid "MapPOIs"
+msgstr "POIs"
+
+msgid "MapPOI__visible__help"
+msgstr "Will this POI be shown on the map?"
+
+msgid "MapPOI__visible"
+msgstr "visible"
+
+msgid "MapPOI__name__help"
+msgstr "descriptive name of this POI"
+
+msgid "MapPOI__name"
+msgstr "name"
+
+msgid "MapPOI__description__help"
+msgstr "public information"
+
+msgid "MapPOI__description"
+msgstr "description"
+
+msgid "MapPOI__is_official__help"
+msgstr "This POI is relevant for the conference (e.g. cash desk, info desk)."
+
+msgid "MapPOI__is_official"
+msgstr "official"
+
+msgid "MapPOI__location_layer__help"
+msgstr "The layer this POI is on."
+
+msgid "MapPOI__location_layer"
+msgstr "layer"
+
+msgid "MapPOI__location_point__help"
+msgstr "point on the map where this POI is located"
+
+msgid "MapPOI__location_point"
+msgstr "location"
+
+msgid "MapPOI__need-both-layer-and-point"
+msgstr "Need both layer and point, one is not enough."
+
 msgid "DirectMessage__sender__help"
 msgstr "user which sent this message (empty = system)"
 
diff --git a/src/core/migrations/0118_map.py b/src/core/migrations/0118_map.py
new file mode 100644
index 0000000000000000000000000000000000000000..550b405fee8152c2c23027ca76d3e4a86d972abd
--- /dev/null
+++ b/src/core/migrations/0118_map.py
@@ -0,0 +1,74 @@
+# Generated by Django 4.2.6 on 2023-11-04 14:01
+
+import core.fields
+import django.contrib.gis.db.models.fields
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+def create_default_layer(apps, schema_editor):
+    """ create a default layer per conference. """
+    Conference = apps.get_model('core', 'Conference')
+    MapLayer = apps.get_model('core', 'MapLayer')
+    for c in Conference.objects.all():
+        layer = MapLayer(conference=c, index=0, name='Default')
+        layer.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0117_conference_support_channels'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='MapLayer',
+            fields=[
+                ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('index', models.SmallIntegerField(help_text='MapLayer__index__help', verbose_name='MapLayer__index')),
+                ('name', models.CharField(help_text='MapLayer__name__help', max_length=200, verbose_name='MapLayer__name')),
+                ('name_de', models.CharField(help_text='MapLayer__name__help', max_length=200, null=True, verbose_name='MapLayer__name')),
+                ('name_en', models.CharField(help_text='MapLayer__name__help', max_length=200, null=True, verbose_name='MapLayer__name')),
+                ('conference', core.fields.ConferenceReference(help_text='Conference__reference_help', on_delete=django.db.models.deletion.CASCADE, related_name='map_layers', to='core.conference', verbose_name='Conference__reference')),
+            ],
+            options={
+                'verbose_name': 'MapLayer',
+                'verbose_name_plural': 'MapLayers',
+            },
+        ),
+        migrations.AlterModelOptions(
+            name='conferencemember',
+            options={'permissions': [('assembly_team', 'ConferenceMember__permission-assembly_team'), ('channel_team', 'ConferenceMember__permission-channel_team'), ('static_pages', 'ConferenceMember__permission-static_pages'), ('map_edit', 'ConferenceMember__permission-map_edit'), ('platformusers', 'Orga: Users List'), ('rename_platformuser', 'Orga: Rename User'), ('block_platformuser', 'ConferenceMember__permission-block_platformuser'), ('change_conferencemember__active_angel', 'ConferenceMember__permission-change_conferencemember__active_angel'), ('view_platformuser__guardian', 'ConferenceMember__permission-view_platformuser__guardian'), ('voucher_admin', 'ConferenceMember__permission-voucher_admin'), ('scheduleadmin', 'ConferenceMember__permission-scheduleadmin'), ('workadventure_admin', 'ConferenceMember__permission-workadventure_admin')]},
+        ),
+        migrations.CreateModel(
+            name='MapPOI',
+            fields=[
+                ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+                ('visible', models.BooleanField(default=True, help_text='MapPOI__visible__help', verbose_name='MapPOI__visible')),
+                ('name', models.CharField(help_text='MapPOI__name__help', max_length=200, verbose_name='MapPOI__name')),
+                ('name_de', models.CharField(help_text='MapPOI__name__help', max_length=200, null=True, verbose_name='MapPOI__name')),
+                ('name_en', models.CharField(help_text='MapPOI__name__help', max_length=200, null=True, verbose_name='MapPOI__name')),
+                ('description', models.TextField(blank=True, help_text='MapPOI__description__help', null=True, verbose_name='MapPOI__description')),
+                ('description_de', models.TextField(blank=True, help_text='MapPOI__description__help', null=True, verbose_name='MapPOI__description')),
+                ('description_en', models.TextField(blank=True, help_text='MapPOI__description__help', null=True, verbose_name='MapPOI__description')),
+                ('description_html', models.TextField(blank=True)),
+                ('description_html_de', models.TextField(blank=True, null=True)),
+                ('description_html_en', models.TextField(blank=True, null=True)),
+                ('is_official', models.BooleanField(default=False, help_text='MapPOI__is_official__help', verbose_name='MapPOI__is_official')),
+                ('location_point', django.contrib.gis.db.models.fields.PointField(blank=True, help_text='MapPOI__location_point__help', null=True, srid=4326, verbose_name='MapPOI__location_point')),
+                ('conference', core.fields.ConferenceReference(help_text='Conference__reference_help', on_delete=django.db.models.deletion.CASCADE, related_name='pois', to='core.conference', verbose_name='Conference__reference')),
+                ('location_layer', models.ForeignKey(blank=True, help_text='MapPOI__location_layer__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pois', to='core.maplayer', verbose_name='MapPOI__location_layer')),
+            ],
+            options={
+                'verbose_name': 'MapPOI',
+                'verbose_name_plural': 'MapPOIs',
+            },
+        ),
+        migrations.AddField(
+            model_name='assembly',
+            name='location_layer',
+            field=models.ForeignKey(blank=True, help_text='Assembly__location_layer__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assemblies', to='core.maplayer', verbose_name='Assembly__location_layer'),
+        ),
+    ]
diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py
index a8aead8c1e25aeb7fadec36564122773ea065e4e..8c514fa8ba47dc4190ee99d7db667a7f819ff6b0 100644
--- a/src/core/models/__init__.py
+++ b/src/core/models/__init__.py
@@ -6,6 +6,7 @@ from .board import BulletinBoardEntry
 from .events import Event, EventAttachment, EventLikeCount, EventParticipant
 from .pages import StaticPage, StaticPageRevision
 from .markdown import MarkdownMeta
+from .map import MapLayer, MapPOI
 from .messages import DirectMessage
 from .rooms import Room, RoomLink
 from .schedules import ScheduleSource, ScheduleSourceImport, ScheduleSourceMapping
@@ -27,6 +28,7 @@ __all__ = [
     'ConferenceTag', 'ConferenceTrack',
     'DereferrerStats', 'DirectMessage',
     'Event', 'EventAttachment', 'EventLikeCount', 'EventParticipant',
+    'MapLayer', 'MapPOI',
     'PlatformUser',
     'Room', 'RoomLink',
     'ScheduleSource', 'ScheduleSourceImport', 'ScheduleSourceMapping',
diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py
index 6b76ce22cfdd4796c7017af3fd48c3d7568c7e94..6aa98f711250c11d05b907fb6c295c2fceefa008 100644
--- a/src/core/models/assemblies.py
+++ b/src/core/models/assemblies.py
@@ -22,6 +22,7 @@ from ..fields import ConferenceReference
 from ..markdown import compile_translated_markdown_fields, render_markdown, store_relationships
 from ..utils import render_markdown_as_text
 from .conference import Conference, ConferenceMember
+from .map import MapLayer
 from .tags import TaggedItemMixin
 from .users import PlatformUser
 
@@ -246,6 +247,14 @@ class Assembly(TaggedItemMixin, models.Model):
         verbose_name=_('Assembly__public_contact'),
     )
 
+    location_layer = models.ForeignKey(
+        MapLayer,
+        blank=True, null=True,
+        related_name='assemblies', on_delete=models.PROTECT,
+        help_text=_('Assembly__location_layer__help'),
+        verbose_name=_('Assembly__location_layer'),
+    )
+
     location_point = gis_models.PointField(
         blank=True, null=True,
         help_text=_('Assembly__location_point__help'),
@@ -519,6 +528,9 @@ class Assembly(TaggedItemMixin, models.Model):
         has_for_all_assemblies = Voucher.objects.available_for_conference(self.conference).exists()
         return 0 if has_for_all_assemblies else None
 
+    def get_location_layer_index(self):
+        return self.location_layer.index if self.location_layer else None
+
     def get_location_point_as_json(self):
         if not self.location_point or not self.location_point.valid:
             return ''
diff --git a/src/core/models/conference.py b/src/core/models/conference.py
index c562663f010141ae2ef2845605666f48a56642c1..45c906dc20f1ea81d436a2d0c9ce2c9e0fc70b39 100644
--- a/src/core/models/conference.py
+++ b/src/core/models/conference.py
@@ -64,6 +64,9 @@ class ConferenceMember(models.Model):
             ('static_pages', _('ConferenceMember__permission-static_pages')),
             # Access to static pages, can be further limited by configuring static_page_groups.
 
+            ('map_edit', _('ConferenceMember__permission-map_edit')),
+            # modification of the map, i.e. POIs
+
             ('platformusers', 'Orga: Users List'),
             ('rename_platformuser', 'Orga: Rename User'),
 
diff --git a/src/core/models/map.py b/src/core/models/map.py
new file mode 100644
index 0000000000000000000000000000000000000000..56864f99ee2a8e8da69cf710bf62731398cee88b
--- /dev/null
+++ b/src/core/models/map.py
@@ -0,0 +1,93 @@
+import json
+import logging
+from uuid import uuid4
+
+from django.contrib.gis.db import models as gis_models
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from ..fields import ConferenceReference
+from ..markdown import compile_translated_markdown_fields, store_relationships
+
+
+logger = logging.getLogger(__name__)
+
+
+class MapLayer(models.Model):
+    class Meta:
+        verbose_name = _('MapLayer')
+        verbose_name_plural = _('MapLayers')
+
+    id = models.UUIDField(default=uuid4, primary_key=True, editable=False, serialize=False)
+    conference = ConferenceReference(related_name='map_layers')
+
+    index = models.SmallIntegerField(help_text=_('MapLayer__index__help'), verbose_name=_('MapLayer__index'))
+    name = models.CharField(
+        max_length=200,
+        help_text=_('MapLayer__name__help'),
+        verbose_name=_('MapLayer__name'))
+
+
+class MapPOI(models.Model):
+    class Meta:
+        verbose_name = _('MapPOI')
+        verbose_name_plural = _('MapPOIs')
+
+    id = models.UUIDField(default=uuid4, primary_key=True, editable=False, serialize=False)
+    conference = ConferenceReference(related_name='pois')
+
+    visible = models.BooleanField(
+        default=True,
+        help_text=_('MapPOI__visible__help'),
+        verbose_name=_('MapPOI__visible'))
+
+    name = models.CharField(
+        max_length=200,
+        help_text=_('MapPOI__name__help'),
+        verbose_name=_('MapPOI__name'))
+    description = models.TextField(
+        blank=True, null=True,
+        help_text=_('MapPOI__description__help'),
+        verbose_name=_('MapPOI__description'))
+    description_html = models.TextField(blank=True)
+
+    is_official = models.BooleanField(
+        default=False,
+        help_text=_('MapPOI__is_official__help'),
+        verbose_name=_('MapPOI__is_official'))
+
+    location_layer = models.ForeignKey(
+        MapLayer,
+        blank=True, null=True,
+        related_name='pois', on_delete=models.PROTECT,
+        help_text=_('MapPOI__location_layer__help'),
+        verbose_name=_('MapPOI__location_layer'),
+    )
+
+    location_point = gis_models.PointField(
+        blank=True, null=True,
+        help_text=_('MapPOI__location_point__help'),
+        verbose_name=_('MapPOI__location_point'),
+    )
+
+    def clean(self):
+        if self.location_point or self.location_layer:
+            if not self.location_point or not self.location_layer:
+                raise ValidationError(_('MapPOI__need-both-layer-and-point'))
+
+    def save(self, *args, update_fields=None, **kwargs):
+        if update_fields is None or 'description' in update_fields:
+            render_results = compile_translated_markdown_fields(self, self.conference, 'description')
+            store_relationships(self.conference, self, render_results)
+
+        return super().save(*args, update_fields=update_fields, **kwargs)
+
+    def get_location_layer_index(self):
+        return self.location_layer.index if self.location_layer else None
+
+    def get_location_point_as_json(self):
+        if not self.location_point or not self.location_point.valid:
+            return ''
+
+        return json.dumps([self.location_point.x, self.location_point.y])
diff --git a/src/core/translation.py b/src/core/translation.py
index 2c08c1e040e47b001001fc1531ea842941a0924a..f10c55b4f60b7ed5db3fbe45c7e70aaf59c87eab 100644
--- a/src/core/translation.py
+++ b/src/core/translation.py
@@ -6,6 +6,7 @@ from .models import \
     ConferenceMember, \
     ConferenceNavigationItem, \
     Event, \
+    MapLayer, MapPOI, \
     Room
 
 
@@ -37,3 +38,13 @@ class ConferenceMemberTranslationOptions(TranslationOptions):
 @register(Room)
 class RoomTranslationOptions(TranslationOptions):
     fields = ('description', 'description_html', )
+
+
+@register(MapLayer)
+class MapLayerTranslationOptions(TranslationOptions):
+    fields = ('name', )
+
+
+@register(MapPOI)
+class MapPOITranslationOptions(TranslationOptions):
+    fields = ('name', 'description', 'description_html', )