diff --git a/src/api/tests/map.py b/src/api/tests/map.py
index 41bf4fe15d9dc949121c452615c12c074e2adeef..173260088398caef19e5b4c324583c953efc061f 100644
--- a/src/api/tests/map.py
+++ b/src/api/tests/map.py
@@ -49,6 +49,7 @@ class MapTest(TestCase):
 
         # set POI on assembly and accept it
         self.assembly.location_point = Point(8.9, 52.9)
+        self.assembly.location_state = Assembly.LocationState.ROUGH
         self.assembly.state = Assembly.State.ACCEPTED
         self.assembly.save()
         ConferenceExportCache.objects.all().delete()
@@ -60,7 +61,7 @@ class MapTest(TestCase):
         self.assertEqual(0, len(content['features']))
 
         # change state to 'placed', now there should be a POI
-        self.assembly.state = Assembly.State.PLACED
+        self.assembly.location_state = Assembly.LocationState.FINAL
         self.assembly.save()
         ConferenceExportCache.objects.all().delete()
         resp = self.client.get(url)
diff --git a/src/api/views/maps.py b/src/api/views/maps.py
index 245e265d5bef40efad8a46341f33e99e81b244d3..77b9209605e3e6e19f90e07056ac0e5ce795a491 100644
--- a/src/api/views/maps.py
+++ b/src/api/views/maps.py
@@ -5,9 +5,10 @@ from rest_framework.response import Response
 from rest_framework.views import APIView
 
 from django.contrib.gis.gdal import CoordTransform, SpatialReference
+from django.db.models import Q, QuerySet
 
 from core.models.assemblies import Assembly
-from core.models.conference import ConferenceExportCache
+from core.models.conference import Conference, ConferenceExportCache
 from core.models.map import MapPOI
 
 from api.permissions import IsConferenceService, IsSuperUser
@@ -16,6 +17,13 @@ from api.views.mixins import ConferenceSlugMixin
 _cts = {}  # cache of CoordTransforms (if needed)
 
 
+def get_exportable_assemblies(conference: Conference) -> QuerySet[Assembly]:
+    """Fetches all assemblies in the given conference with publish-able location."""
+    exportable_states = [*Assembly.PUBLIC_STATES, Assembly.State.HIDDEN]
+    q_placed_assemblies = Q(location_state__in=[Assembly.LocationState.PREVIEW, Assembly.LocationState.FINAL])
+    return Assembly.objects.filter(conference=conference, state__in=exportable_states).filter(q_placed_assemblies)
+
+
 def get_field_geojson(value, srid: int, ct_cache: dict | None = None):
     if value.srid != srid:
         if ct_cache is None:
@@ -70,8 +78,7 @@ class AssembliesExportView(ConferenceSlugMixin, APIView, metaclass=abc.ABCMeta):
     geometry_field = None
 
     def get_queryset(self):
-        exportable_states = [*Assembly.PLACED_STATES, Assembly.State.HIDDEN]
-        return Assembly.objects.filter(conference=self.conference, state__in=exportable_states)
+        return get_exportable_assemblies(conference=self.conference)
 
     def get_geometry_field(self, obj):
         return getattr(obj, self.geometry_field)
@@ -137,8 +144,7 @@ class C3NavExportView(ConferenceSlugMixin, APIView):
 
         data = []
 
-        exportable_states = [*Assembly.PLACED_STATES, Assembly.State.HIDDEN]
-        qs = self.conference.assemblies.filter(state__in=exportable_states)
+        qs = get_exportable_assemblies(self.conference)
         if request.GET.get('all') != '1':
             qs = qs.exclude(location_data__point=None, location_data__boundaries=None)
         for assembly in qs.all():  # type: Assembly
@@ -153,8 +159,11 @@ class C3NavExportView(ConferenceSlugMixin, APIView):
                     'description': {'de': assembly.description_de, 'en': assembly.description_en},
                     'public_url': hub_absolute('plainui:assembly', assembly_slug=assembly.slug),
                     'parent_id': assembly.parent_id,
-                    'children': assembly.children.filter(state__in=exportable_states).values_list('slug', flat=True) if assembly.is_cluster else None,
+                    'children': get_exportable_assemblies(conference=self.conference).filter(parent=assembly).values_list('slug', flat=True)
+                    if assembly.is_cluster
+                    else None,
                     'floor': assembly.get_location_floor_index(),
+                    'is_preview': assembly.location_state != Assembly.LocationState.FINAL,
                     'location': loc_data.get('point'),  # assembly.get_location_point_xy(),
                     'polygons': loc_data.get('boundaries'),  # assembly.get_location_boundaries_xy(),
                 }
diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po
index 08f86f2090567158ea086cd8f8469406a5e2a1ca..b3cc091661bc1c6d3fe776d10e327e0fec9fdb66 100644
--- a/src/backoffice/locale/de/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/de/LC_MESSAGES/django.po
@@ -646,9 +646,6 @@ msgstr "verstecken"
 msgid "Assembly__edit__state_btn_rejected2registered"
 msgstr "doch in Erwägung ziehen"
 
-msgid "Assembly__edit__state_btn_placed"
-msgstr "platziert"
-
 msgid "Assembly__edit__state_btn_arrived"
 msgstr "angekommen (eigene Aussage)"
 
@@ -664,6 +661,30 @@ msgstr "Assembly bearbeiten"
 msgid "AssemblyTeam__message"
 msgstr "Nachricht senden"
 
+msgid "AssemblyTeam__position__status"
+msgstr "Status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "Umrandung"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "Vorschau"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "Entwurf"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "nur Assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "nicht verf."
+
 msgid "AssemblyTeam__lastnote"
 msgstr "letzte Notiz"
 
@@ -695,20 +716,43 @@ msgstr ""
 msgid "Assembly__edit__position_inform_assembly"
 msgstr "Verwaltende der Assemblies informieren"
 
-msgid "Assembly__edit__position_update"
-msgstr "Position aktualisieren"
+msgid "AssemblyTeam__edit_position-save"
+msgstr "Position speichern im Status:"
 
-msgid "Assembly__edit__position_recall"
-msgstr "Veröffentlichung zurücknehmen"
+msgid "AssemblyTeam__edit_position__tooltip-none"
+msgstr "Als 'keine Position' behandeln, auch wenn ggf. bereits eine gespeichert ist - z.B. ein alter Standort."
 
-msgid "Assembly__edit__position_publish"
-msgstr "mit Position veröffentlichen"
+# use translation from core
+msgid "Assembly__location_state-none"
+msgstr ""
 
-msgid "Assembly__edit__position_save_unpublished"
-msgstr "Position vormerken"
+msgid "AssemblyTeam__edit_position__tooltip-draft"
+msgstr "Position ist eine Idee / nur grober Entwurf (nur für Assembly-Orga sichtbar)."
 
-msgid "Assembly__edit__position_save_unaccepted"
-msgstr "Position für nicht akzeptierte Assembly speichern"
+# use translation from core
+msgid "Assembly__location_state-draft"
+msgstr ""
+
+msgid "AssemblyTeam__edit_position__tooltip-rough"
+msgstr "Werte als grobe Position speichern (nur im Maschinenraum für die Assembly und ggf. Habitat sichtbar)."
+
+# use translation from core
+msgid "Assembly__location_state-rough"
+msgstr ""
+
+msgid "AssemblyTeam__edit_position__tooltip-preview"
+msgstr "Werte als grobe Position speichern (wird mit diesem Hinweis auch bereits veröffentlicht)."
+
+# use translation from core
+msgid "Assembly__location_state-preview"
+msgstr ""
+
+msgid "AssemblyTeam__edit_position__tooltip-final"
+msgstr "Finale bzw. öffentliche Position speichern."
+
+# use translation from core
+msgid "Assembly__location_state-final"
+msgstr ""
 
 msgid "Assembly__edit__state"
 msgstr "Anmelde-Status der Assembly ändern"
@@ -1916,14 +1960,8 @@ msgstr "Es gab einen Fehler beim Parsen der Position, bitte erneut probieren."
 msgid "Assembly__edit__position_missing_on_publish"
 msgstr "Veröffentlichung abgebrochen: keine Positionsdaten gefunden"
 
-msgid "Assembly__edit__position_was_not_published"
-msgstr "Die Assembly war gar nicht veröffentlicht?!"
-
-msgid "Assembly__edit__position_unknown_action"
-msgstr "Unbekannte Aktion, Position wurde dennoch gespeichert."
-
-msgid "Assembly__edit__changed_position"
-msgstr "Position der Assembly wurde aktualisiert"
+msgid "AssemblyTeam__edit_position__success"
+msgstr "Die Position wurde erfolgreich aktualisert."
 
 #, python-format
 msgid "Badge__create__successful %(badge)s"
diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po
index a07f265e18d6390bfa569a347137506f377137cb..750a6417b354d2be5a65f13c32b07b266e130182 100644
--- a/src/backoffice/locale/en/LC_MESSAGES/django.po
+++ b/src/backoffice/locale/en/LC_MESSAGES/django.po
@@ -646,9 +646,6 @@ msgstr "hide"
 msgid "Assembly__edit__state_btn_rejected2registered"
 msgstr "consider it"
 
-msgid "Assembly__edit__state_btn_placed"
-msgstr "placed"
-
 msgid "Assembly__edit__state_btn_arrived"
 msgstr "arrived (self)"
 
@@ -664,6 +661,30 @@ msgstr "edit assembly"
 msgid "AssemblyTeam__message"
 msgstr "send a message"
 
+msgid "AssemblyTeam__position__status"
+msgstr "status"
+
+msgid "AssemblyTeam__position__point"
+msgstr "POI"
+
+msgid "AssemblyTeam__position__boundary"
+msgstr "boundary"
+
+msgid "AssemblyTeam__position-final"
+msgstr "final"
+
+msgid "AssemblyTeam__position-preview"
+msgstr "preview"
+
+msgid "AssemblyTeam__position-draft"
+msgstr "draft"
+
+msgid "AssemblyTeam__position-internal"
+msgstr "assembly"
+
+msgid "AssemblyTeam__position-none"
+msgstr "none"
+
 msgid "AssemblyTeam__lastnote"
 msgstr "latest note"
 
@@ -695,20 +716,43 @@ msgstr ""
 msgid "Assembly__edit__position_inform_assembly"
 msgstr "inform the assembly's management"
 
-msgid "Assembly__edit__position_update"
-msgstr "update position"
+msgid "AssemblyTeam__edit_position-save"
+msgstr "save the position with this state:"
+
+msgid "AssemblyTeam__edit_position__tooltip-none"
+msgstr "handle this assembly as 'no position', even if a location may be set (e.g. old one)"
+
+# use translation from core
+msgid "Assembly__location_state-none"
+msgstr ""
+
+msgid "AssemblyTeam__edit_position__tooltip-draft"
+msgstr "draft position, assembly team only"
+
+# use translation from core
+msgid "Assembly__location_state-draft"
+msgstr ""
+
+msgid "AssemblyTeam__edit_position__tooltip-rough"
+msgstr "rough position, will be visible here in the backoffice only (i.e. with assemblies/habitats, too, but not external services like e.g. c3nav)"
+
+# use translation from core
+msgid "Assembly__location_state-rough"
+msgstr ""
 
-msgid "Assembly__edit__position_recall"
-msgstr "recall the assembly"
+msgid "AssemblyTeam__edit_position__tooltip-preview"
+msgstr "rough position, will be shared with external services like e.g. c3nav"
 
-msgid "Assembly__edit__position_publish"
-msgstr "publish the assembly"
+# use translation from core
+msgid "Assembly__location_state-preview"
+msgstr "change assembly's registration state"
 
-msgid "Assembly__edit__position_save_unpublished"
-msgstr "save location (w/o publish)"
+msgid "AssemblyTeam__edit_position__tooltip-final"
+msgstr "final position, will be shared publicly"
 
-msgid "Assembly__edit__position_save_unaccepted"
-msgstr "save location for yet-unaccepted assembly"
+# use translation from core
+msgid "Assembly__location_state-final"
+msgstr ""
 
 msgid "Assembly__edit__state"
 msgstr "change assembly's registration state"
@@ -1921,14 +1965,8 @@ msgstr "The given location data could not be parsed."
 msgid "Assembly__edit__position_missing_on_publish"
 msgstr "Could not publish the assembly as there was no location data present."
 
-msgid "Assembly__edit__position_was_not_published"
-msgstr "Could not recall the assembly: it was not published?!"
-
-msgid "Assembly__edit__position_unknown_action"
-msgstr "Unknown action requested, location was updated nonetheless."
-
-msgid "Assembly__edit__changed_position"
-msgstr "Location of the assembly was updated."
+msgid "AssemblyTeam__edit_position__success"
+msgstr "The assembly's position has been updated successfully."
 
 #, python-format
 msgid "Badge__create__successful %(badge)s"
diff --git a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
index 96eec278f4c0d9161ec0ff381d6b216136dff0e6..64df76b5d2aec3a7236e8381157ee59a2c4e0878 100644
--- a/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
+++ b/src/backoffice/templates/backoffice/assemblyteam_assembly_detail.html
@@ -147,15 +147,6 @@
 
                   {% else %}
                     {% if assembly.is_public %}
-                      {% if assembly.state != 'placed' %}
-                        <li>
-                          <a class="dropdown-item text-secondary"
-                             role="button"
-                             href="{% url 'backoffice:assemblyteam-editstate' pk=assembly.id %}?state=placed">
-                            {% trans "Assembly__edit__state_btn_placed" %}
-                          </a>
-                        </li>
-                      {% endif %}
                       {% if assembly.state != 'arrived' %}
                         <li>
                           <a class="dropdown-item text-info"
@@ -224,22 +215,38 @@
           <div class="card-body d-flex flex-row">
             <div>
               <label for="pos_public">
-                <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "public" %}:
+                <i class="bi bi-eye{% if not object.is_placed %}-slash{% endif %}"></i> {% trans "AssemblyTeam__position__status" %}:
               </label>
               <br>
               <label for="pos_point">
-                <i class="bi bi-pin-map"></i> POI:
+                <i class="bi bi-pin-map"></i> {% trans "AssemblyTeam__position__point" %}:
               </label>
               <br>
               <label for="pos_boundary">
-                <i class="bi bi-map"></i> Boundary:
+                <i class="bi bi-map"></i> {% trans "AssemblyTeam__position__boundary" %}
               </label>
             </div>
             <div class="flex-grow-1 ps-3">
-              {% if object.is_placed %}
-                <span id="pos_public" class="text-success"><i class="bi bi-check-square"></i> {% trans "yes" %}</span>
+              {% if object.location_state == object.LocationState.FINAL %}
+                <span id="pos_public"
+                      class="text-success"
+                      title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-final" %}</span>
+              {% elif object.location_state == object.LocationState.PREVIEW %}
+                <span id="pos_public"
+                      class="text-success"
+                      title="{{ object.get_location_state_display }}"><i class="bi bi-check-square"></i> {% trans "AssemblyTeam__position-preview" %}</span>
+              {% elif object.location_state == object.LocationState.DRAFT %}
+                <span id="pos_public"
+                      class="text-warning"
+                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-draft" %}</span>
+              {% elif object.location_state == object.LocationState.ROUGH %}
+                <span id="pos_public"
+                      class="text-warning"
+                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-internal" %}</span>
               {% else %}
-                <span id="pos_public" class="text-warning"><i class="bi bi-x-square"></i> {% trans "no" %}</span>
+                <span id="pos_public"
+                      class="text-danger"
+                      title="{{ object.get_location_state_display }}"><i class="bi bi-x-square"></i> {% trans "AssemblyTeam__position-none" %}</span>
               {% endif %}
               <br>
               {% if object.location_data.point %}
diff --git a/src/backoffice/templates/backoffice/assemblyteam_editposition.html b/src/backoffice/templates/backoffice/assemblyteam_editposition.html
index 83a33fe28583df5aadb402e47c005630d99b474b..c12dde635e12dc0808157e425b0f82410d96a6da 100644
--- a/src/backoffice/templates/backoffice/assemblyteam_editposition.html
+++ b/src/backoffice/templates/backoffice/assemblyteam_editposition.html
@@ -79,35 +79,44 @@
                       placeholder="(optional)"
                       autocomplete="off"></textarea>
 
-            {% if assembly.is_placed %}
+            <label class="col-sm-2 col-form-label text-muted" for="buttons">{% trans "AssemblyTeam__edit_position-save" %}</label>
+            <div class="btn-group" id="buttons">
               <button type="submit"
-                      class="btn btn-sm btn-primary"
-                      name="action"
-                      value="save">{% trans "Assembly__edit__position_update" %}</button>
-
+                      class="btn btn-sm {% if assembly.location_state == 'none' %}btn-primary{% else %}btn-outline-danger{% endif %}"
+                      name="location_state"
+                      value="none"
+                      title="{% trans "AssemblyTeam__edit_position__tooltip-none" %}">
+                {% trans "Assembly__location_state-none" %}
+              </button>
               <button type="submit"
-                      class="btn btn-sm btn-danger"
-                      name="action"
-                      value="recall">{% trans "Assembly__edit__position_recall" %}</button>
-
-            {% elif assembly.is_public %}
+                      class="btn btn-sm {% if assembly.location_state == 'draft' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-success{% elif assembly.location_state == 'rough' %}btn-outline-warning{% else %}btn-outline-danger{% endif %}"
+                      name="location_state"
+                      value="draft"
+                      title="{% trans "AssemblyTeam__edit_position__tooltip-draft" %}">
+                {% trans "Assembly__location_state-draft" %}
+              </button>
               <button type="submit"
-                      class="btn btn-sm btn-primary"
-                      name="action"
-                      value="publish">{% trans "Assembly__edit__position_publish" %}</button>
-
+                      class="btn btn-sm {% if assembly.location_state == 'rough' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-success{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
+                      name="location_state"
+                      value="rough"
+                      title="{% trans "AssemblyTeam__edit_position__tooltip-rough" %}">
+                {% trans "Assembly__location_state-rough" %}
+              </button>
               <button type="submit"
-                      class="btn btn-sm btn-secondary"
-                      name="action"
-                      value="save">{% trans "Assembly__edit__position_save_unpublished" %}</button>
-
-            {% else %}
+                      class="btn btn-sm {% if assembly.location_state == 'preview' %}btn-primary{% elif assembly.location_state == 'draft' %}btn-outline-warning{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'final' %}btn-outline-warning{% else %}btn-outline-primary{% endif %}"
+                      name="location_state"
+                      value="preview"
+                      title="{% trans "AssemblyTeam__edit_position__tooltip-preview" %}">
+                {% trans "Assembly__location_state-preview" %}
+              </button>
               <button type="submit"
-                      class="btn btn-sm btn-danger"
-                      name="action"
-                      value="save">{% trans "Assembly__edit__position_save_unaccepted" %}</button>
-
-            {% endif %}
+                      class="btn btn-sm {% if assembly.location_state == 'final' %}btn-primary{% elif assembly.location_state == 'none' %}btn-outline-danger{% elif assembly.location_state == 'draft' %}btn-outline-danger{% else %}btn-outline-primary{% endif %}"
+                      name="location_state"
+                      value="final"
+                      title="{% trans "AssemblyTeam__edit_position__tooltip-final" %}">
+                {% trans "Assembly__location_state-final" %}
+              </button>
+            </div>
           </form>
         </div>
       </div>
diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py
index b3ceb9af6149da55edbcf45447b6301293242469..c7f1201d865df555db5c553dcea617fb07a4d6c8 100644
--- a/src/backoffice/urls.py
+++ b/src/backoffice/urls.py
@@ -62,6 +62,7 @@ urlpatterns = [
     path('wiki/page/<uuid:pk>/delete', wiki.PageDeleteView.as_view(), name='wiki-page-delete'),
     path('wiki/page/<uuid:pk>/delete-revision', wiki.PageRevisionDeleteView.as_view(), name='wiki-page-revision-delete'),
     path('assemblies', assemblyteam.AssembliesView.as_view(), name='assemblies'),
+    path('assemblies/floor.geojson', assemblies.AssemblyGeoJsonExportView.as_view(), name='assemblies-floor-geojson'),
     path('assemblies/list/<str:variant>', assemblyteam.AssembliesListsView.as_view(), name='assemblieslist'),
     path('assemblyteam/<uuid:pk>', assemblyteam.AssemblyView.as_view(), name='assemblyteam-detail'),
     path('assemblyteam/<uuid:pk>/state', assemblyteam.AssemblyEditStateView.as_view(), name='assemblyteam-editstate'),
diff --git a/src/backoffice/views/assemblies/__init__.py b/src/backoffice/views/assemblies/__init__.py
index 3cc45e5da6afb2dbc608425b9abb8200a6172e1c..718271192081cba034e6a4b6fb3de19dc18e5e13 100644
--- a/src/backoffice/views/assemblies/__init__.py
+++ b/src/backoffice/views/assemblies/__init__.py
@@ -13,6 +13,9 @@ from backoffice.views.assemblies.auth import (
     AuthView,
     VouchersView,
 )
+from backoffice.views.assemblies.map import (
+    AssemblyGeoJsonExportView,
+)
 from backoffice.views.assemblies.members import (
     MemberCreateView,
     MemberListView,
@@ -31,6 +34,7 @@ __all__ = [
     'AssemblyChildrenUpdateView',
     'AssemblyCreateView',
     'AssemblyDetailView',
+    'AssemblyGeoJsonExportView',
     'AssemblyLinksUpdateView',
     'AssemblyListView',
     'AssemblyParentLeaveView',
diff --git a/src/backoffice/views/assemblies/map.py b/src/backoffice/views/assemblies/map.py
new file mode 100644
index 0000000000000000000000000000000000000000..27f563ae6d79d70a37e54edc9c314f7df97416c8
--- /dev/null
+++ b/src/backoffice/views/assemblies/map.py
@@ -0,0 +1,51 @@
+from django.http import HttpResponseBadRequest, JsonResponse
+from django.views import View
+
+from core.models import Assembly
+
+from backoffice.views.mixins import ConferenceLoginRequiredMixin
+
+
+def flip_coordinates(coordinates):
+    """Flips the coordinates' tuples' items as latitude/longitude in GeoJSON represents to Y/X instead of X/Y."""
+    result = []
+    for polygon in coordinates:
+        p = []
+        for coord in polygon:
+            p.append((coord[1], coord[0]))
+        result.append(p)
+    return result
+
+
+class AssemblyGeoJsonExportView(ConferenceLoginRequiredMixin, View):
+    def get(self, request):
+        floor = request.GET.get('floor')
+        if floor is None:
+            return HttpResponseBadRequest('Missing "floor" parameter.')
+
+        features = []
+
+        qs = Assembly.objects.conference_accessible(self.conference, self.conferencemember).filter(location_floor__index=floor)
+        qs = qs.exclude(location_data__point=None, location_data__boundaries=None)
+
+        for assembly in qs:
+            if coords := assembly.location_data.get('boundaries'):
+                feature = {
+                    'type': 'Feature',
+                    'geometry': {
+                        'type': 'Polygon',
+                        'coordinates': flip_coordinates(coords),
+                    },
+                    'properties': {
+                        'name': assembly.name,
+                        'slug': assembly.slug,
+                        'published': assembly.is_placed,
+                    },
+                }
+                features.append(feature)
+
+        result = {
+            'type': 'FeatureCollection',
+            'features': features,
+        }
+        return JsonResponse(result, content_type='application/vnd.geo+json', json_dumps_params={'indent': 2})
diff --git a/src/backoffice/views/assemblyteam.py b/src/backoffice/views/assemblyteam.py
index 40cedbb0c1e3dcd056f2440a6b533ef096204c1c..42b35b619d1917f846ade973a2ed31a343a8a043 100644
--- a/src/backoffice/views/assemblyteam.py
+++ b/src/backoffice/views/assemblyteam.py
@@ -566,7 +566,7 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
         request = self.request
         assembly = self.assembly
 
-        action = request.POST.get('action', 'save')
+        location_state = request.POST.get('location_state', None)
         inform_assembly = request.POST.get('inform_assembly', None) is not None
         poi = request.POST.get('location_point', None)
         boundaries = request.POST.get('location_boundaries', None)
@@ -574,9 +574,9 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
         comment = request.POST.get('comment', '').strip()
 
         old_values = {
-            'state': assembly.state,
-            'location_point': assembly.get_location_point_xy() or '',
-            'location_boundaries': assembly.get_location_boundaries_xy() or '',
+            'location_state': assembly.location_state,
+            'location_point': str(assembly.get_location_point_xy() or ''),
+            'location_boundaries': str(assembly.get_location_boundaries_xy() or ''),
             'location_floor': str(assembly.get_location_floor_index() or ''),
         }
         changes = {}
@@ -621,34 +621,24 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
             messages.error(request, gettext('Assembly__edit__position_error'))
             return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk)
 
-        if action == 'publish':
-            # if assembly.location_point is not None or assembly.location_boundaries is not None:
-            if assembly.location_data.get('point') or assembly.location_data.get('boundaries'):
-                assembly.state = Assembly.State.PLACED
-                changes['state'] = assembly.state
-            else:
+        if location_state and location_state != assembly.location_state:
+            if location_state != Assembly.LocationState.NONE and not (assembly.location_data.get('point') or assembly.location_data.get('boundaries')):
                 messages.warning(request, gettext('Assembly__edit__position_missing_on_publish'))
                 return redirect('backoffice:assemblyteam-editposition', pk=assembly.pk)
 
-        elif action == 'recall':
-            if assembly.is_placed:
-                assembly.state = Assembly.State.ACCEPTED
-                changes['state'] = assembly.state
-            else:
-                messages.warning(request, gettext('Assembly__edit__position_was_not_published'))
-
-        elif action != 'save':
-            messages.warning(request, gettext('Assembly__edit__position_unknown_action'))
+            assembly.location_state = location_state
+            changes['location_state'] = location_state
 
-        assembly.save(update_fields=['state', 'location_point', 'location_boundaries', 'location_floor', 'location_data'])
+        assembly.save(update_fields=['location_state', 'location_point', 'location_boundaries', 'location_floor', 'location_data'])
 
         # log the action
-        messages.success(request, gettext('Assembly__edit__changed_position'))
+        messages.success(request, gettext('AssemblyTeam__edit_position__success'))
         logger.info(
-            'Assembly "%(assembly_slug)s" (%(assembly_pk)s): set POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.',  # noqa:E501
+            'Assembly "%(assembly_slug)s" (%(assembly_pk)s): set location_state to "%(location_state)s, POI to "%(poi)s" & boundaries to "%(boundaries)s" & floor to "%(floor)s" upon request by <%(user)s>.',  # noqa:E501
             {
                 'assembly_slug': assembly.slug,
                 'assembly_pk': assembly.pk,
+                'location_state': location_state,
                 'floor': floor,
                 'poi': poi,
                 'boundaries': boundaries,
@@ -662,18 +652,6 @@ class AssemblyEditPlacementView(SingleAssemblyTeamMixin, View):
             **{k: ActivityLogChange(old=old_values[k], new=v) for k, v in changes.items() if old_values[k] != v},
         )
 
-        if assembly.state != old_values['state']:
-            messages.success(request, gettext('Assembly__edit__changed_state'))
-            logger.info(
-                'Assembly "%(assembly_slug)s" (%(assembly_pk)s): changed state to "%(state)s" upon request by <%(user)s>.',
-                {
-                    'assembly_slug': assembly.slug,
-                    'assembly_pk': assembly.pk,
-                    'state': assembly.state,
-                    'user': self.request.user.username,
-                },
-            )
-
         # invalidate export caches where necessary
         ConferenceExportCache.signal_assembly_modification(conference=self.conference, assembly=assembly)
 
diff --git a/src/core/admin.py b/src/core/admin.py
index 216cefcfa5c5cd90120f02881de53950b16d0b83..c00e2f344b16a5603d58bd58a4a1ca6cb4c48b4d 100644
--- a/src/core/admin.py
+++ b/src/core/admin.py
@@ -378,9 +378,9 @@ class ActivityLogEntryInline(GenericTabularInline):
 
 
 class AssemblyAdmin(GISModelAdmin):
-    list_display = ['conference', 'parent', 'slug', 'name', 'state', 'is_official']
+    list_display = ['conference', 'parent', 'slug', 'name', 'state', 'location_state', 'is_official']
     list_display_links = ['slug', 'name']
-    list_filter = ['conference', 'parent', 'state', 'is_official']
+    list_filter = ['conference', 'parent', 'state', 'location_state', 'is_official']
     readonly_fields = ['id', 'conference']
     search_fields = ['slug', 'name', 'description']
     inlines = [TagsInline, BadgeInline, AssemblyLinkInline, AssemblyMemberInline, ActivityLogEntryInline]
@@ -407,7 +407,7 @@ class AssemblyAdmin(GISModelAdmin):
         (
             'Location',
             {
-                'fields': ['assembly_location', 'location_point', 'location_boundaries'],
+                'fields': ['location_state', 'assembly_location', 'location_point', 'location_boundaries', 'location_data'],
             },
         ),
     )
diff --git a/src/core/locale/de/LC_MESSAGES/django.po b/src/core/locale/de/LC_MESSAGES/django.po
index aee8a920424d3546646e892efd7a84b515d2d475..54605f17f955c3b137a75835b2e6c296b318595e 100644
--- a/src/core/locale/de/LC_MESSAGES/django.po
+++ b/src/core/locale/de/LC_MESSAGES/django.po
@@ -281,6 +281,21 @@ msgstr "Habitat"
 msgid "Assembly__hierarchy__cluster_restricted"
 msgstr "Habitat (nicht öffentlich)"
 
+msgid "Assembly__location_state-none"
+msgstr "nicht verfügbar"
+
+msgid "Assembly__location_state-draft"
+msgstr "Entwurf/Idee (Orga only)"
+
+msgid "Assembly__location_state-rough"
+msgstr "grob (intern)"
+
+msgid "Assembly__location_state-preview"
+msgstr "grob (öffentliche Vorschau)"
+
+msgid "Assembly__location_state-final"
+msgstr "final (öffentlich)"
+
 msgid "Assembly__slug__help"
 msgstr "Name der Assembly für URLs, bestehend aus Buchstaben, Nummern, Unterstrichen und Bindestrichen"
 
@@ -381,6 +396,12 @@ msgstr "öffentlich sichtbare E-Mail für Kontakt"
 msgid "Assembly__public_contact"
 msgstr "öffentlicher Kontakt"
 
+msgid "Assembly__location_state__help"
+msgstr "Qualität der ausgewählten Position auf der Karte"
+
+msgid "Assembly__location_state"
+msgstr "Position (Karte) Status"
+
 msgid "Assembly__location_floor__help"
 msgstr "Ebene, auf der sich die Assembly befindet"
 
diff --git a/src/core/locale/en/LC_MESSAGES/django.po b/src/core/locale/en/LC_MESSAGES/django.po
index b66a8888e0460b2825bbb7051250afd0b57cd999..d5c6b5e3d29d407e90a308af459a86bdf22ae8f1 100644
--- a/src/core/locale/en/LC_MESSAGES/django.po
+++ b/src/core/locale/en/LC_MESSAGES/django.po
@@ -281,6 +281,21 @@ msgstr "habitat"
 msgid "Assembly__hierarchy__cluster_restricted"
 msgstr "habitat (non-public)"
 
+msgid "Assembly__location_state-none"
+msgstr "not available"
+
+msgid "Assembly__location_state-draft"
+msgstr "draft (orga only)"
+
+msgid "Assembly__location_state-rough"
+msgstr "rough (internal)"
+
+msgid "Assembly__location_state-preview"
+msgstr "rough (public preview)"
+
+msgid "Assembly__location_state-final"
+msgstr "final (public)"
+
 msgid "Assembly__slug__help"
 msgstr "short name of this assembly used in URLs, consisting of letters, numbers, underscores and hyphens"
 
@@ -381,6 +396,12 @@ msgstr "publicly visible email for contact"
 msgid "Assembly__public_contact"
 msgstr "public contact"
 
+msgid "Assembly__location_state__help"
+msgstr "quality of the selected position, how to handle it?"
+
+msgid "Assembly__location_state"
+msgstr "location state"
+
 msgid "Assembly__location_floor__help"
 msgstr "floor, on which this assembly is placed"
 
diff --git a/src/core/migrations/0172_assembly_location_state.py b/src/core/migrations/0172_assembly_location_state.py
new file mode 100644
index 0000000000000000000000000000000000000000..1fc3d11690572d52ecf2c7539c9af149dccedfa5
--- /dev/null
+++ b/src/core/migrations/0172_assembly_location_state.py
@@ -0,0 +1,32 @@
+# Generated by Django 5.1.3 on 2024-12-23 11:13
+
+from django.db import migrations, models
+
+
+def setup_location_state(apps, schema_editor):
+    Assembly = apps.get_model("core", "Assembly")  # type=core.models.Assembly
+    for a in Assembly.objects.all():
+        if a.location_data:
+            if a.state in ['placed', 'arrived', 'confirmed']:  # aka a.is_placed
+                a.location_state = 'final' if a.state == 'placed' else 'preview'
+            else:
+                a.location_state = 'draft'
+        else:
+            a.location_state = 'none'
+        a.save(update_fields=["location_state"])
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0171_remove_team_require_staff_team_visibility'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='assembly',
+            name='location_state',
+            field=models.CharField(choices=[('none', 'Assembly__location_state-none'), ('draft', 'Assembly__location_state-draft'), ('rough', 'Assembly__location_state-rough'), ('preview', 'Assembly__location_state-preview'), ('final', 'Assembly__location_state-final')], default='none', help_text='Assembly__location_state__help', max_length=20, verbose_name='Assembly__location_state'),
+        ),
+        migrations.RunPython(setup_location_state, migrations.RunPython.noop, elidable=True),
+    ]
diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py
index 292948f540dcc0fbc5893ba0d4ba51281dde8625..8990771ec281287afde06b9e82804aa0fa103c52 100644
--- a/src/core/models/assemblies.py
+++ b/src/core/models/assemblies.py
@@ -98,7 +98,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel):
         HIDDEN = 'hidden', _('Assembly__state-hidden')
         ACCEPTED = 'accepted', _('Assembly__state-accepted')
         REJECTED = 'rejected', _('Assembly__state-rejected')
-        PLACED = 'placed', _('Assembly__state-placed')
+        PLACED = 'placed', _('Assembly__state-placed')  # TODO: entfernen, obsolet mit location_state
         ARRIVED_SELF = 'arrived', _('Assembly__state-arrived_self')
         ARRIVED_CONFIRMED = 'confirmed', _('Assembly__state-arrived_confirmed')
 
@@ -107,6 +107,18 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel):
         CLUSTER = 'cluster', _('Assembly__hierarchy__cluster')
         CLUSTER_RESTRICTED = 'clusterrestricted', _('Assembly__hierarchy__cluster_restricted')
 
+    class LocationState(models.TextChoices):
+        NONE = 'none', _('Assembly__location_state-none')
+        """No location set."""
+        DRAFT = 'draft', _('Assembly__location_state-draft')
+        """Internal preview, i.e. assembly orga only."""
+        ROUGH = 'rough', _('Assembly__location_state-rough')
+        """Rough location idea, hub-internal only."""
+        PREVIEW = 'preview', _('Assembly__location_state-preview')
+        """Rough location idea, shared with integrations like c3nav."""
+        FINAL = 'final', _('Assembly__location_state-final')
+        """Precise location data, shared with the world."""
+
     objects = AssemblyManager()
     logger = logging.getLogger(__name__)
 
@@ -201,6 +213,13 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel):
         related_query_name='assemblies_invited',
     )
 
+    location_state = models.CharField(
+        max_length=20,
+        choices=LocationState.choices,
+        default=LocationState.NONE,
+        help_text=_('Assembly__location_state__help'),
+        verbose_name=_('Assembly__location_state'),
+    )
     location_floor = models.ForeignKey(
         MapFloor,
         blank=True,
@@ -266,11 +285,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel):
         State.ARRIVED_SELF.value,
     ]
 
-    PLACED_STATES = [
-        State.PLACED.value,
-        State.ARRIVED_CONFIRMED.value,
-        State.ARRIVED_SELF.value,
-    ]
+    PLACED_LOCATION_STATES = [LocationState.PREVIEW, LocationState.FINAL]
 
     def __str__(self):
         return self.name
@@ -361,7 +376,7 @@ class Assembly(TaggedItemMixin, ActivityLogMixin, RulesModel):
 
     @cached_property
     def is_placed(self):
-        return self.state in self.PLACED_STATES
+        return self.location_state in self.PLACED_LOCATION_STATES
 
     @cached_property
     def is_cluster(self):
diff --git a/src/core/templates/core/map.html b/src/core/templates/core/map.html
index 8b976f4f8a5247c74024a93fae0b92080490f931..931542903c5c69d6c7c5d20c3ad9fec1e3c13e7f 100644
--- a/src/core/templates/core/map.html
+++ b/src/core/templates/core/map.html
@@ -45,7 +45,15 @@
           "{{ floor.name }}": floor{{ floor.code_slug }},
         {% endfor %}
       };
-      var overlays = {};
+
+      var assemblies = L.geoJSON().addTo(map);
+
+      var overlays = {
+        assemblies,
+      };
+
+      loadAssemblies(initial_floor_idx);
+
       const layerOptions = {
         collapsed: false,
         hideSingleBase: true,
@@ -123,10 +131,61 @@
         }
       {% endif %}
 
+      function styleAssemblies(feature) {
+        if (feature.properties.published) {
+          return {
+            color: "#CCFFCC",
+            weight: 2,
+          }
+        } else {
+          return {
+            color: "#FFCCCC",
+            weight: 1,
+            dashArray: "4 4",
+            dashOffset: "0",
+          }
+        }
+      }
+
+      function handleAssemblyFeature (feature, layer) {
+        layer.bindTooltip(
+          '<b>' + feature.properties.slug + '</b><br>' +
+          feature.properties.name +
+          (feature.properties.published ? '' : '<br>NOT PUBLISHED'),
+          {permanent: false, opacity: 0.75},
+        )
+      }
+
+      function loadAssemblies(activeFloor) {
+        map.removeLayer(assemblies);
+        //overlays = overlays.filter(item => item !== assemblies);  // overlays.remove(assemblies);
+
+        var url = "{% url "backoffice:assemblies-floor-geojson" %}?floor=" + activeFloor
+        fetch(url).then((response) => {
+          if (!response.ok) {
+            throw new Error(`HTTP error! Status: ${response.status}`);
+          }
+          return response.json();
+        })
+          .then((data) => {
+            console.log('got GeoJSON for assemblies on floor', activeFloor, '=', data);
+            assemblies = L.geoJSON(data, {
+              onEachFeature: handleAssemblyFeature,
+              style: styleAssemblies,
+            }).addTo(map);
+            console.log('new assemblies layer:', assemblies);
+            overlays.push(assemblies);
+
+            boundariesLayer.bringToFront();
+            poiLayer.bringToFront();
+          });
+      }
+
       function updateFloor(idx) {
         activeFloor = idx;
         const floor_elem = document.getElementById('{{ floor_id }}');
         floor_elem.value = idx != null ? idx.toString() : "";
+        loadAssemblies(activeFloor);
       }
 
       function updatePoiPosition(latLng) {