diff --git a/BauSystem/BauSystem_RegionDynamic/src/de/steamwar/bausystem/region/dynamic/MultiTileArea.java b/BauSystem/BauSystem_RegionDynamic/src/de/steamwar/bausystem/region/dynamic/MultiTileArea.java
new file mode 100644
index 00000000..b8a7ed39
--- /dev/null
+++ b/BauSystem/BauSystem_RegionDynamic/src/de/steamwar/bausystem/region/dynamic/MultiTileArea.java
@@ -0,0 +1,188 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2026 SteamWar.de-Serverteam
+ *
+ * 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
+ * (at your option) 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 .
+ */
+
+package de.steamwar.bausystem.region.dynamic;
+
+import com.sk89q.worldedit.extent.clipboard.Clipboard;
+import de.steamwar.bausystem.region.Point;
+import de.steamwar.bausystem.region.Region;
+import de.steamwar.bausystem.shared.Pair;
+import lombok.NonNull;
+import org.bukkit.Location;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+
+public abstract class MultiTileArea implements Region.Area {
+
+ private final Set tiles = new HashSet<>();
+
+ public boolean addTile(Tile tile) {
+ boolean result = tiles.add(tile);
+ quantize();
+ return result;
+ }
+
+ public boolean removeTile(Tile tile) {
+ boolean result = tiles.remove(tile);
+ quantize();
+ return result;
+ }
+
+ public void quantize() {
+ long time = System.currentTimeMillis();
+ Quantizer quantizer = new Quantizer(tiles);
+ List> results = new ArrayList<>();
+ while (true) {
+ Pair pair = quantizer.next();
+ if (pair == null) break;
+ results.add(pair);
+ }
+ // [{"min_tile_x":0,"min_tile_z":0,"max_tile_x":0,"max_tile_z":0}]
+ time = System.currentTimeMillis() - time;
+ System.out.println(tiles.size() + ": " + results.size() + " in " + time + "ms");
+ }
+
+ @Override
+ public @NonNull Point getMinPoint(boolean extension) {
+ return Point.ZERO;
+ }
+
+ @Override
+ public @NonNull Point getMaxPoint(boolean extension) {
+ return Point.ZERO;
+ }
+
+ @Override
+ public @NonNull Point getCopyPoint() {
+ return Point.ZERO;
+ }
+
+ @Override
+ public boolean inRegion(Location location, boolean extension) {
+ return inRegion(location.getBlockX(), location.getBlockZ(), extension);
+ }
+
+ @Override
+ public boolean inRegion(int x, int z, boolean extension) {
+ Tile tile = Tile.fromXZ(x, z).orElse(null);
+ return tiles.contains(tile);
+ }
+
+ @Override
+ public @Nullable Clipboard copy(boolean extension) {
+ return null;
+ }
+
+ @Override
+ public void forEachChunk(BiConsumer executor) {
+ }
+
+ @Override
+ public boolean isChunkOutside(int chunkX, int chunkZ) {
+ return true;
+ }
+
+ private static class Quantizer {
+
+ private Map tileToNeighbourCount = new HashMap<>();
+
+ public Quantizer(Set tiles) {
+ for (Tile tile : tiles) {
+ int count = (int) tile.neighboursRing()
+ .filter(tiles::contains)
+ .count();
+ tileToNeighbourCount.put(tile, count);
+ }
+ }
+
+ public Pair next() {
+ Map.Entry entry = tileToNeighbourCount.entrySet()
+ .stream()
+ .max(Map.Entry.comparingByValue())
+ .orElse(null);
+ if (entry == null) return null;
+
+ Tile minTile = entry.getKey();
+ Tile maxTile = entry.getKey();
+ tileToNeighbourCount.remove(entry.getKey());
+
+ while (true) {
+ Pair, Integer> neg_x = tryExpand(minTile, maxTile, -1, 0);
+ Pair, Integer> pos_x = tryExpand(minTile, maxTile, 1, 0);
+ Pair, Integer> neg_z = tryExpand(minTile, maxTile, 0, -1);
+ Pair, Integer> pos_z = tryExpand(minTile, maxTile, 0, 1);
+
+ if (neg_x.getValue() == 0 && pos_x.getValue() == 0 && neg_z.getValue() == 0 && pos_z.getValue() == 0) {
+ break;
+ }
+
+ Set remove;
+ if (neg_x.getValue() >= pos_x.getValue() && neg_x.getValue() >= neg_z.getValue() && neg_x.getValue() >= pos_z.getValue()) {
+ minTile = minTile.add(-1, 0).orElseThrow();
+ remove = neg_x.getKey();
+ } else if (pos_x.getValue() >= neg_x.getValue() && pos_x.getValue() >= neg_z.getValue() && pos_x.getValue() >= pos_z.getValue()) {
+ maxTile = maxTile.add(1, 0).orElseThrow();
+ remove = pos_x.getKey();
+ } else if (neg_z.getValue() >= neg_x.getValue() && neg_z.getValue() >= pos_x.getValue() && neg_z.getValue() >= pos_z.getValue()) {
+ minTile = minTile.add(0, -1).orElseThrow();
+ remove = neg_z.getKey();
+ } else {
+ maxTile = maxTile.add(0, 1).orElseThrow();
+ remove = pos_z.getKey();
+ }
+
+ remove.forEach(tileToNeighbourCount::remove);
+ }
+
+ return new Pair<>(minTile, minTile.equals(maxTile) ? null : maxTile);
+ }
+
+ private static final Pair, Integer> EMPTY = new Pair<>(Collections.emptySet(), 0);
+
+ private Pair, Integer> tryExpand(Tile minTile, Tile maxTile, int dx, int dz) {
+ Set expaned = new HashSet<>();
+ if (dx == 0) {
+ for (int x = minTile.getTileX(); x <= maxTile.getTileX(); x++) {
+ int z = (dz < 0 ? minTile.getTileZ() : maxTile.getTileZ()) + dz;
+ Tile tile = Tile.fromTile(x, z).orElse(null);
+ if (tile == null) return EMPTY;
+ if (!tileToNeighbourCount.containsKey(tile)) return EMPTY;
+ expaned.add(tile);
+ }
+ } else if (dz == 0) {
+ for (int z = minTile.getTileZ(); z <= maxTile.getTileZ(); z++) {
+ int x = (dx < 0 ? minTile.getTileX() : maxTile.getTileX()) + dx;
+ Tile tile = Tile.fromTile(x, z).orElse(null);
+ if (tile == null) return EMPTY;
+ if (!tileToNeighbourCount.containsKey(tile)) return EMPTY;
+ expaned.add(tile);
+ }
+ } else {
+ throw new IllegalStateException();
+ }
+
+ int total = expaned.stream()
+ .mapToInt(tileToNeighbourCount::get)
+ .sum();
+ return new Pair<>(expaned, total);
+ }
+ }
+}