--- a/net/minecraft/world/level/portal/PortalForcer.java
+++ b/net/minecraft/world/level/portal/PortalForcer.java
@@ -1,6 +1,7 @@
 package net.minecraft.world.level.portal;
 
 import java.util.Comparator;
+import java.util.Iterator;
 import java.util.Optional;
 import net.minecraft.BlockUtil;
 import net.minecraft.core.BlockPos;
@@ -20,6 +21,7 @@
 import net.minecraft.world.level.levelgen.Heightmap;
 
 public class PortalForcer {
+
     public static final int TICKET_RADIUS = 3;
     private static final int SEARCH_RADIUS = 128;
     private static final int CREATE_RADIUS = 16;
@@ -40,72 +42,119 @@
     }
 
     public Optional<BlockUtil.FoundRectangle> findPortalAround(BlockPos pos, boolean destIsNether, WorldBorder worldBorder) {
-        PoiManager poiManager = this.level.getPoiManager();
-        int i = destIsNether ? 16 : 128;
-        poiManager.ensureLoadedAndValid(this.level, pos, i);
-        Optional<PoiRecord> optional = poiManager.getInSquare(poiType -> poiType.is(PoiTypes.NETHER_PORTAL), pos, i, PoiManager.Occupancy.ANY)
-            .filter(poi -> worldBorder.isWithinBounds(poi.getPos()))
-            .sorted(Comparator.<PoiRecord>comparingDouble(poi -> poi.getPos().distSqr(pos)).thenComparingInt(poi -> poi.getPos().getY()))
-            .filter(poi -> this.level.getBlockState(poi.getPos()).hasProperty(BlockStateProperties.HORIZONTAL_AXIS))
-            .findFirst();
-        return optional.map(
-            poi -> {
-                BlockPos blockPos = poi.getPos();
-                this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockPos), 3, blockPos);
-                BlockState blockState = this.level.getBlockState(blockPos);
-                return BlockUtil.getLargestRectangleAround(
-                    blockPos,
-                    blockState.getValue(BlockStateProperties.HORIZONTAL_AXIS),
-                    21,
-                    Direction.Axis.Y,
-                    21,
-                    posx -> this.level.getBlockState(posx) == blockState
-                );
-            }
+        // CraftBukkit start
+        return this.findPortalAround(pos, worldBorder, destIsNether ? level.paperConfig().environment.portalCreateRadius : level.paperConfig().environment.portalSearchRadius); // Search Radius // Paper - Configurable portal search radius
+    }
+
+    public Optional<BlockUtil.FoundRectangle> findPortalAround(BlockPos blockposition, WorldBorder worldborder, int i) {
+        PoiManager villageplace = this.level.getPoiManager();
+        // int i = flag ? 16 : 128;
+        // CraftBukkit end
+
+        // Paper start - optimise portals
+        Optional<PoiRecord> optional;
+        java.util.List<PoiRecord> records = new java.util.ArrayList<>();
+        io.papermc.paper.util.PoiAccess.findClosestPoiDataRecords(
+            villageplace,
+            type -> type.is(PoiTypes.NETHER_PORTAL),
+            (BlockPos pos) -> {
+                net.minecraft.world.level.chunk.ChunkAccess lowest = this.level.getChunk(pos.getX() >> 4, pos.getZ() >> 4, net.minecraft.world.level.chunk.status.ChunkStatus.EMPTY);
+                if (!lowest.getStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.FULL)
+                    && (lowest.getBelowZeroRetrogen() == null || !lowest.getBelowZeroRetrogen().targetStatus().isOrAfter(net.minecraft.world.level.chunk.status.ChunkStatus.SPAWN))) {
+                    // why would we generate the chunk?
+                    return false;
+                }
+                if (!worldborder.isWithinBounds(pos) || (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.test(v -> pos.getY() >= v))) { // Paper - Configurable nether ceiling damage
+                    return false;
+                }
+                return lowest.getBlockState(pos).hasProperty(BlockStateProperties.HORIZONTAL_AXIS);
+            },
+            blockposition, i, Double.MAX_VALUE, PoiManager.Occupancy.ANY, true, records
         );
+
+        // this gets us most of the way there, but we bias towards lower y values.
+        PoiRecord lowestYRecord = null;
+        for (PoiRecord record : records) {
+            if (lowestYRecord == null) {
+                lowestYRecord = record;
+            } else if (lowestYRecord.getPos().getY() > record.getPos().getY()) {
+                lowestYRecord = record;
+            }
+        }
+        // now we're done
+        optional = Optional.ofNullable(lowestYRecord);
+        // Paper end - optimise portals
+
+        return optional.map((villageplacerecord) -> {
+            BlockPos blockposition1 = villageplacerecord.getPos();
+
+            this.level.getChunkSource().addRegionTicket(TicketType.PORTAL, new ChunkPos(blockposition1), 3, blockposition1);
+            BlockState iblockdata = this.level.getBlockState(blockposition1);
+
+            return BlockUtil.getLargestRectangleAround(blockposition1, (Direction.Axis) iblockdata.getValue(BlockStateProperties.HORIZONTAL_AXIS), 21, Direction.Axis.Y, 21, (blockposition2) -> {
+                return this.level.getBlockState(blockposition2) == iblockdata;
+            });
+        });
     }
 
     public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos pos, Direction.Axis axis) {
-        Direction direction = Direction.get(Direction.AxisDirection.POSITIVE, axis);
-        double d = -1.0;
-        BlockPos blockPos = null;
-        double e = -1.0;
-        BlockPos blockPos2 = null;
-        WorldBorder worldBorder = this.level.getWorldBorder();
+        // CraftBukkit start
+        return this.createPortal(pos, axis, null, 16);
+    }
+
+    public Optional<BlockUtil.FoundRectangle> createPortal(BlockPos blockposition, Direction.Axis enumdirection_enumaxis, net.minecraft.world.entity.Entity entity, int createRadius) {
+        // CraftBukkit end
+        Direction enumdirection = Direction.get(Direction.AxisDirection.POSITIVE, enumdirection_enumaxis);
+        double d0 = -1.0D;
+        BlockPos blockposition1 = null;
+        double d1 = -1.0D;
+        BlockPos blockposition2 = null;
+        WorldBorder worldborder = this.level.getWorldBorder();
         int i = Math.min(this.level.getMaxBuildHeight(), this.level.getMinBuildHeight() + this.level.getLogicalHeight()) - 1;
-        BlockPos.MutableBlockPos mutableBlockPos = pos.mutable();
+        // Paper start - Configurable nether ceiling damage; make sure the max height doesn't exceed the void damage height
+        if (this.level.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.NETHER && this.level.paperConfig().environment.netherCeilingVoidDamageHeight.enabled()) {
+            i = Math.min(i, this.level.paperConfig().environment.netherCeilingVoidDamageHeight.intValue() - 1);
+        }
+        // Paper end - Configurable nether ceiling damage
+        BlockPos.MutableBlockPos blockposition_mutableblockposition = blockposition.mutable();
+        Iterator iterator = BlockPos.spiralAround(blockposition, createRadius, Direction.EAST, Direction.SOUTH).iterator(); // CraftBukkit
 
-        for (BlockPos.MutableBlockPos mutableBlockPos2 : BlockPos.spiralAround(pos, 16, Direction.EAST, Direction.SOUTH)) {
-            int j = Math.min(i, this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, mutableBlockPos2.getX(), mutableBlockPos2.getZ()));
-            int k = 1;
-            if (worldBorder.isWithinBounds(mutableBlockPos2) && worldBorder.isWithinBounds(mutableBlockPos2.move(direction, 1))) {
-                mutableBlockPos2.move(direction.getOpposite(), 1);
+        int j;
+        int k;
+        int l;
 
-                for (int l = j; l >= this.level.getMinBuildHeight(); l--) {
-                    mutableBlockPos2.setY(l);
-                    if (this.canPortalReplaceBlock(mutableBlockPos2)) {
-                        int m = l;
+        while (iterator.hasNext()) {
+            BlockPos.MutableBlockPos blockposition_mutableblockposition1 = (BlockPos.MutableBlockPos) iterator.next();
 
-                        while (l > this.level.getMinBuildHeight() && this.canPortalReplaceBlock(mutableBlockPos2.move(Direction.DOWN))) {
-                            l--;
+            j = Math.min(i, this.level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockposition_mutableblockposition1.getX(), blockposition_mutableblockposition1.getZ()));
+            boolean flag = true;
+
+            if (worldborder.isWithinBounds((BlockPos) blockposition_mutableblockposition1) && worldborder.isWithinBounds((BlockPos) blockposition_mutableblockposition1.move(enumdirection, 1))) {
+                blockposition_mutableblockposition1.move(enumdirection.getOpposite(), 1);
+
+                for (k = j; k >= this.level.getMinBuildHeight(); --k) {
+                    blockposition_mutableblockposition1.setY(k);
+                    if (this.canPortalReplaceBlock(blockposition_mutableblockposition1)) {
+                        for (l = k; k > this.level.getMinBuildHeight() && this.canPortalReplaceBlock(blockposition_mutableblockposition1.move(Direction.DOWN)); --k) {
+                            ;
                         }
 
-                        if (l + 4 <= i) {
-                            int n = m - l;
-                            if (n <= 0 || n >= 3) {
-                                mutableBlockPos2.setY(l);
-                                if (this.canHostFrame(mutableBlockPos2, mutableBlockPos, direction, 0)) {
-                                    double f = pos.distSqr(mutableBlockPos2);
-                                    if (this.canHostFrame(mutableBlockPos2, mutableBlockPos, direction, -1)
-                                        && this.canHostFrame(mutableBlockPos2, mutableBlockPos, direction, 1)
-                                        && (d == -1.0 || d > f)) {
-                                        d = f;
-                                        blockPos = mutableBlockPos2.immutable();
+                        if (k + 4 <= i) {
+                            int i1 = l - k;
+
+                            if (i1 <= 0 || i1 >= 3) {
+                                blockposition_mutableblockposition1.setY(k);
+                                if (this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, 0)) {
+                                    double d2 = blockposition.distSqr(blockposition_mutableblockposition1);
+
+                                    if (this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, -1) && this.canHostFrame(blockposition_mutableblockposition1, blockposition_mutableblockposition, enumdirection, 1) && (d0 == -1.0D || d0 > d2)) {
+                                        d0 = d2;
+                                        blockposition1 = blockposition_mutableblockposition1.immutable();
                                     }
 
-                                    if (d == -1.0 && (e == -1.0 || e > f)) {
-                                        e = f;
-                                        blockPos2 = mutableBlockPos2.immutable();
+                                    if (d0 == -1.0D && (d1 == -1.0D || d1 > d2)) {
+                                        d1 = d2;
+                                        blockposition2 = blockposition_mutableblockposition1.immutable();
                                     }
                                 }
                             }
@@ -115,79 +164,96 @@
             }
         }
 
-        if (d == -1.0 && e != -1.0) {
-            blockPos = blockPos2;
-            d = e;
+        if (d0 == -1.0D && d1 != -1.0D) {
+            blockposition1 = blockposition2;
+            d0 = d1;
         }
 
-        if (d == -1.0) {
-            int o = Math.max(this.level.getMinBuildHeight() - -1, 70);
-            int p = i - 9;
-            if (p < o) {
+        int j1;
+        int k1;
+
+        org.bukkit.craftbukkit.util.BlockStateListPopulator blockList = new org.bukkit.craftbukkit.util.BlockStateListPopulator(this.level); // CraftBukkit - Use BlockStateListPopulator
+        if (d0 == -1.0D) {
+            j1 = Math.max(this.level.getMinBuildHeight() - -1, 70);
+            k1 = i - 9;
+            if (k1 < j1) {
                 return Optional.empty();
             }
 
-            blockPos = new BlockPos(pos.getX(), Mth.clamp(pos.getY(), o, p), pos.getZ()).immutable();
-            Direction direction2 = direction.getClockWise();
-            if (!worldBorder.isWithinBounds(blockPos)) {
+            blockposition1 = (new BlockPos(blockposition.getX(), Mth.clamp(blockposition.getY(), j1, k1), blockposition.getZ())).immutable();
+            Direction enumdirection1 = enumdirection.getClockWise();
+
+            if (!worldborder.isWithinBounds(blockposition1)) {
                 return Optional.empty();
             }
 
-            for (int q = -1; q < 2; q++) {
-                for (int r = 0; r < 2; r++) {
-                    for (int s = -1; s < 3; s++) {
-                        BlockState blockState = s < 0 ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.AIR.defaultBlockState();
-                        mutableBlockPos.setWithOffset(
-                            blockPos, r * direction.getStepX() + q * direction2.getStepX(), s, r * direction.getStepZ() + q * direction2.getStepZ()
-                        );
-                        this.level.setBlockAndUpdate(mutableBlockPos, blockState);
+            for (int l1 = -1; l1 < 2; ++l1) {
+                for (k = 0; k < 2; ++k) {
+                    for (l = -1; l < 3; ++l) {
+                        BlockState iblockdata = l < 0 ? Blocks.OBSIDIAN.defaultBlockState() : Blocks.AIR.defaultBlockState();
+
+                        blockposition_mutableblockposition.setWithOffset(blockposition1, k * enumdirection.getStepX() + l1 * enumdirection1.getStepX(), l, k * enumdirection.getStepZ() + l1 * enumdirection1.getStepZ());
+                        blockList.setBlock(blockposition_mutableblockposition, iblockdata, 3); // CraftBukkit
                     }
                 }
             }
         }
 
-        for (int t = -1; t < 3; t++) {
-            for (int u = -1; u < 4; u++) {
-                if (t == -1 || t == 2 || u == -1 || u == 3) {
-                    mutableBlockPos.setWithOffset(blockPos, t * direction.getStepX(), u, t * direction.getStepZ());
-                    this.level.setBlock(mutableBlockPos, Blocks.OBSIDIAN.defaultBlockState(), 3);
+        for (j1 = -1; j1 < 3; ++j1) {
+            for (k1 = -1; k1 < 4; ++k1) {
+                if (j1 == -1 || j1 == 2 || k1 == -1 || k1 == 3) {
+                    blockposition_mutableblockposition.setWithOffset(blockposition1, j1 * enumdirection.getStepX(), k1, j1 * enumdirection.getStepZ());
+                    blockList.setBlock(blockposition_mutableblockposition, Blocks.OBSIDIAN.defaultBlockState(), 3); // CraftBukkit
                 }
             }
         }
 
-        BlockState blockState2 = Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, axis);
+        BlockState iblockdata1 = (BlockState) Blocks.NETHER_PORTAL.defaultBlockState().setValue(NetherPortalBlock.AXIS, enumdirection_enumaxis);
 
-        for (int v = 0; v < 2; v++) {
-            for (int w = 0; w < 3; w++) {
-                mutableBlockPos.setWithOffset(blockPos, v * direction.getStepX(), w, v * direction.getStepZ());
-                this.level.setBlock(mutableBlockPos, blockState2, 18);
+        for (k1 = 0; k1 < 2; ++k1) {
+            for (j = 0; j < 3; ++j) {
+                blockposition_mutableblockposition.setWithOffset(blockposition1, k1 * enumdirection.getStepX(), j, k1 * enumdirection.getStepZ());
+                blockList.setBlock(blockposition_mutableblockposition, iblockdata1, 18); // CraftBukkit
             }
         }
 
-        return Optional.of(new BlockUtil.FoundRectangle(blockPos.immutable(), 2, 3));
+        // CraftBukkit start
+        org.bukkit.World bworld = this.level.getWorld();
+        org.bukkit.event.world.PortalCreateEvent event = new org.bukkit.event.world.PortalCreateEvent((java.util.List<org.bukkit.block.BlockState>) (java.util.List) blockList.getList(), bworld, (entity == null) ? null : entity.getBukkitEntity(), org.bukkit.event.world.PortalCreateEvent.CreateReason.NETHER_PAIR);
+
+        this.level.getCraftServer().getPluginManager().callEvent(event);
+        if (event.isCancelled()) {
+            return Optional.empty();
+        }
+        blockList.updateList();
+        // CraftBukkit end
+        return Optional.of(new BlockUtil.FoundRectangle(blockposition1.immutable(), 2, 3));
     }
 
     private boolean canPortalReplaceBlock(BlockPos.MutableBlockPos pos) {
-        BlockState blockState = this.level.getBlockState(pos);
-        return blockState.canBeReplaced() && blockState.getFluidState().isEmpty();
+        BlockState iblockdata = this.level.getBlockState(pos);
+
+        return iblockdata.canBeReplaced() && iblockdata.getFluidState().isEmpty();
     }
 
     private boolean canHostFrame(BlockPos pos, BlockPos.MutableBlockPos temp, Direction portalDirection, int distanceOrthogonalToPortal) {
-        Direction direction = portalDirection.getClockWise();
+        Direction enumdirection1 = portalDirection.getClockWise();
 
-        for (int i = -1; i < 3; i++) {
-            for (int j = -1; j < 4; j++) {
-                temp.setWithOffset(
-                    pos,
-                    portalDirection.getStepX() * i + direction.getStepX() * distanceOrthogonalToPortal,
-                    j,
-                    portalDirection.getStepZ() * i + direction.getStepZ() * distanceOrthogonalToPortal
-                );
-                if (j < 0 && !this.level.getBlockState(temp).isSolid()) {
+        for (int j = -1; j < 3; ++j) {
+            for (int k = -1; k < 4; ++k) {
+                temp.setWithOffset(pos, portalDirection.getStepX() * j + enumdirection1.getStepX() * distanceOrthogonalToPortal, k, portalDirection.getStepZ() * j + enumdirection1.getStepZ() * distanceOrthogonalToPortal);
+                // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
+                if (!io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits) {
+                    if (!this.level.getBlockState(temp).isDestroyable()) {
+                        return false;
+                    }
+                }
+                // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+                if (k < 0 && !this.level.getBlockState(temp).isSolid()) {
                     return false;
                 }
 
-                if (j >= 0 && !this.canPortalReplaceBlock(temp)) {
+                if (k >= 0 && !this.canPortalReplaceBlock(temp)) {
                     return false;
                 }
             }
