diff --git a/.gitignore b/.gitignore
index e7bbbdc07d..3bc13c3292 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,3 +72,6 @@ img-export/**/*.png
 nix/result*
 /result*
 /shell.nix
+
+# Bash
+.history
diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs
index 8397cf46c6..a895506e0f 100644
--- a/world/src/civ/mod.rs
+++ b/world/src/civ/mod.rs
@@ -120,6 +120,47 @@ impl ProximityRequirements {
         all_of_compliance && any_of_compliance
     }
 
+    pub fn location_hint(&self, world_dims: &Aabr<i32>) -> Aabr<i32> {
+        let bounding_box_of_point = |point: Vec2<i32>, max_distance: i32| Aabr {
+            min: Vec2 {
+                x: point.x - max_distance,
+                y: point.y - max_distance,
+            },
+            max: Vec2 {
+                x: point.x + max_distance,
+                y: point.y + max_distance,
+            },
+        };
+        let any_of_hint = self
+            .any_of
+            .iter()
+            .fold(None, |acc, spec| match spec.max_distance {
+                None => acc,
+                Some(max_distance) => {
+                    let bounding_box_of_new_point =
+                        bounding_box_of_point(spec.location, max_distance);
+                    match acc {
+                        None => Some(bounding_box_of_new_point),
+                        Some(acc) => Some(acc.union(bounding_box_of_new_point)),
+                    }
+                },
+            })
+            .map(|hint| hint.intersection(*world_dims))
+            .unwrap_or_else(|| world_dims.to_owned());
+        let hint = self
+            .all_of
+            .iter()
+            .fold(any_of_hint, |acc, spec| match spec.max_distance {
+                None => acc,
+                Some(max_distance) => {
+                    let bounding_box_of_new_point =
+                        bounding_box_of_point(spec.location, max_distance);
+                    acc.intersection(bounding_box_of_new_point)
+                },
+            });
+        hint
+    }
+
     pub fn new() -> Self {
         ProximityRequirements {
             all_of: Vec::new(),
@@ -1452,30 +1493,6 @@ fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
     }
 }
 
-/// Return true if a position is suitable for site construction (TODO:
-/// criteria?)
-fn loc_suitable_for_site(
-    sim: &WorldSim,
-    loc: Vec2<i32>,
-    site_kind: SiteKind,
-    is_suitable_loc: bool,
-) -> bool {
-    fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool {
-        for x in (-radius)..radius {
-            for y in (-radius)..radius {
-                let check_loc = loc + Vec2::new(x, y);
-                if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
-                    return false;
-                }
-            }
-        }
-        true
-    }
-    let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
-    // only check occupation if the location is suitable
-    is_suitable_loc && not_occupied()
-}
-
 /// Attempt to search for a location that's suitable for site construction
 fn find_site_loc(
     ctx: &mut GenCtx<impl Rng>,
@@ -1486,16 +1503,24 @@ fn find_site_loc(
     const MAX_ATTEMPTS: usize = 10000;
     let mut loc = None;
     for _ in 0..MAX_ATTEMPTS {
+        let world_dims = Aabr {
+            min: Vec2 { x: 0, y: 0 },
+            max: Vec2 {
+                x: ctx.sim.get_size().x as i32,
+                y: ctx.sim.get_size().y as i32,
+            },
+        };
+        let location_hint = proximity_reqs.location_hint(&world_dims);
         let test_loc = loc.unwrap_or_else(|| {
             Vec2::new(
-                ctx.rng.gen_range(0..ctx.sim.get_size().x as i32),
-                ctx.rng.gen_range(0..ctx.sim.get_size().y as i32),
+                ctx.rng.gen_range(location_hint.min.x..location_hint.max.x),
+                ctx.rng.gen_range(location_hint.min.y..location_hint.max.y),
             )
         });
 
         let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
         if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
-            if loc_suitable_for_site(ctx.sim, test_loc, site_kind, is_suitable_loc) {
+            if site_kind.exclusion_radius_clear(ctx.sim, test_loc) {
                 return Some(test_loc);
             }
 
@@ -1509,6 +1534,110 @@ fn find_site_loc(
     None
 }
 
+fn town_attributes_of_site(loc: Vec2<i32>, sim: &WorldSim) -> Option<TownSiteAttributes> {
+    sim.get(loc).map(|chunk| {
+        const RESOURCE_RADIUS: i32 = 1;
+        let mut river_chunks = 0;
+        let mut lake_chunks = 0;
+        let mut ocean_chunks = 0;
+        let mut rock_chunks = 0;
+        let mut tree_chunks = 0;
+        let mut farmable_chunks = 0;
+        let mut farmable_needs_irrigation_chunks = 0;
+        let mut land_chunks = 0;
+        for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
+            for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
+                let check_loc = loc + Vec2::new(x, y).cpos_to_wpos();
+                sim.get(check_loc).map(|c| {
+                    if num::abs(chunk.alt - c.alt) < 200.0 {
+                        if c.river.is_river() {
+                            river_chunks += 1;
+                        }
+                        if c.river.is_lake() {
+                            lake_chunks += 1;
+                        }
+                        if c.river.is_ocean() {
+                            ocean_chunks += 1;
+                        }
+                        if c.tree_density > 0.7 {
+                            tree_chunks += 1;
+                        }
+                        if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp {
+                            if c.surface_veg > 0.5 {
+                                farmable_chunks += 1;
+                            } else {
+                                match c.get_biome() {
+                                    common::terrain::BiomeKind::Savannah => {
+                                        farmable_needs_irrigation_chunks += 1
+                                    },
+                                    common::terrain::BiomeKind::Desert => {
+                                        farmable_needs_irrigation_chunks += 1
+                                    },
+                                    _ => (),
+                                }
+                            }
+                        }
+                        if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean() {
+                            land_chunks += 1;
+                        }
+                    }
+                    // Mining is different since presumably you dig into the hillside
+                    if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 {
+                        rock_chunks += 1;
+                    }
+                });
+            }
+        }
+        let has_river = river_chunks > 1;
+        let has_lake = lake_chunks > 1;
+        let vegetation_implies_potable_water = chunk.tree_density > 0.4
+            && !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp);
+        let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2;
+        let has_potable_water =
+            { has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water };
+        let has_building_materials = tree_chunks > 0
+            || rock_chunks > 0
+            || chunk.temp > CONFIG.tropical_temp && (has_river || has_lake);
+        let water_rich = lake_chunks + river_chunks > 2;
+        let can_grow_rice = water_rich
+            && chunk.humidity + 1.0 > CONFIG.jungle_hum
+            && chunk.temp + 1.0 > CONFIG.tropical_temp;
+        let farming_score = if can_grow_rice {
+            farmable_chunks * 2
+        } else {
+            farmable_chunks
+        } + if water_rich {
+            farmable_needs_irrigation_chunks
+        } else {
+            0
+        };
+        let fish_score = lake_chunks + ocean_chunks;
+        let food_score = farming_score + fish_score;
+        let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 };
+        let forestry_score = if has_river { tree_chunks } else { 0 };
+        let trading_score = std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks);
+        TownSiteAttributes {
+            food_score,
+            mining_score,
+            forestry_score,
+            trading_score,
+            heating: warm_or_firewood,
+            potable_water: has_potable_water,
+            building_materials: has_building_materials,
+        }
+    })
+}
+
+pub struct TownSiteAttributes {
+    food_score: i32,
+    mining_score: i32,
+    forestry_score: i32,
+    trading_score: i32,
+    heating: bool,
+    potable_water: bool,
+    building_materials: bool,
+}
+
 #[derive(Debug)]
 pub struct Civ {
     capital: Id<Site>,
@@ -1594,99 +1723,19 @@ impl SiteKind {
 
         sim.get(loc).map_or(false, |chunk| {
             let suitable_for_town = |score_threshold: f32| -> bool {
-                const RESOURCE_RADIUS: i32 = 1;
-                let mut river_chunks = 0;
-                let mut lake_chunks = 0;
-                let mut ocean_chunks = 0;
-                let mut rock_chunks = 0;
-                let mut tree_chunks = 0;
-                let mut farmable_chunks = 0;
-                let mut farmable_needs_irrigation_chunks = 0;
-                let mut land_chunks = 0;
-                for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
-                    for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
-                        let check_loc = loc + Vec2::new(x, y).cpos_to_wpos();
-                        sim.get(check_loc).map(|c| {
-                            if num::abs(chunk.alt - c.alt) < 200.0 {
-                                if c.river.is_river() {
-                                    river_chunks += 1;
-                                }
-                                if c.river.is_lake() {
-                                    lake_chunks += 1;
-                                }
-                                if c.river.is_ocean() {
-                                    ocean_chunks += 1;
-                                }
-                                if c.tree_density > 0.7 {
-                                    tree_chunks += 1;
-                                }
-                                if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp {
-                                    if c.surface_veg > 0.5 {
-                                        farmable_chunks += 1;
-                                    } else {
-                                        match c.get_biome() {
-                                            common::terrain::BiomeKind::Savannah => {
-                                                farmable_needs_irrigation_chunks += 1
-                                            },
-                                            common::terrain::BiomeKind::Desert => {
-                                                farmable_needs_irrigation_chunks += 1
-                                            },
-                                            _ => (),
-                                        }
-                                    }
-                                }
-                                if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean()
-                                {
-                                    land_chunks += 1;
-                                }
-                            }
-                            // Mining is different since presumably you dig into the hillside
-                            if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 {
-                                rock_chunks += 1;
-                            }
-                        });
-                    }
-                }
-                let has_river = river_chunks > 1;
-                let has_lake = lake_chunks > 1;
-                let vegetation_implies_potable_water = chunk.tree_density > 0.4
-                    && !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp);
-                let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2;
-                let has_potable_water = {
-                    has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water
-                };
-                let has_building_materials = tree_chunks > 0
-                    || rock_chunks > 0
-                    || chunk.temp > CONFIG.tropical_temp && (has_river || has_lake);
-                let water_rich = lake_chunks + river_chunks > 2;
-                let can_grow_rice = water_rich
-                    && chunk.humidity + 1.0 > CONFIG.jungle_hum
-                    && chunk.temp + 1.0 > CONFIG.tropical_temp;
-                let farming_score = if can_grow_rice {
-                    farmable_chunks * 2
-                } else {
-                    farmable_chunks
-                } + if water_rich {
-                    farmable_needs_irrigation_chunks
-                } else {
-                    0
-                };
-                let fish_score = lake_chunks + ocean_chunks;
-                let food_score = farming_score + fish_score;
-                let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 };
-                let forestry_score = if has_river { tree_chunks } else { 0 };
-                let trading_score =
-                    std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks);
-                let industry_score = 3.0 * (food_score as f32 + 1.0).log2()
-                    + 2.0 * (forestry_score as f32 + 1.0).log2()
-                    + (mining_score as f32 + 1.0).log2()
-                    + (trading_score as f32 + 1.0).log2();
-                has_potable_water
-                    && has_building_materials
-                    && industry_score > score_threshold
-                    && warm_or_firewood
-                    // Because of how the algorithm for site2 towns work, they have to start on land.
-                    && on_land()
+                let attributes = town_attributes_of_site(loc, sim);
+                attributes.map_or(false, |attr| {
+                    let industry_score = 3.0 * (attr.food_score as f32 + 1.0).log2()
+                        + 2.0 * (attr.forestry_score as f32 + 1.0).log2()
+                        + (attr.mining_score as f32 + 1.0).log2()
+                        + (attr.trading_score as f32 + 1.0).log2();
+                    attr.potable_water
+                        && attr.building_materials
+                        && industry_score > score_threshold
+                        && attr.heating
+                        // Because of how the algorithm for site2 towns work, they have to start on land.
+                        && on_land()
+                })
             };
             match self {
                 SiteKind::Gnarling => {
@@ -1756,9 +1805,7 @@ impl SiteKind {
             }
         })
     }
-}
 
-impl SiteKind {
     pub fn exclusion_radius(&self) -> i32 {
         // FIXME: Provide specific values for each individual SiteKind
         match self {
@@ -1766,6 +1813,19 @@ impl SiteKind {
             _ => 8, // This is just an arbitrary value
         }
     }
+
+    pub fn exclusion_radius_clear(&self, sim: &WorldSim, loc: Vec2<i32>) -> bool {
+        let radius = self.exclusion_radius();
+        for x in (-radius)..radius {
+            for y in (-radius)..radius {
+                let check_loc = loc + Vec2::new(x, y);
+                if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
+                    return false;
+                }
+            }
+        }
+        true
+    }
 }
 
 impl Site {
@@ -1827,4 +1887,31 @@ mod tests {
         assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 }));
         assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 }));
     }
+
+    #[test]
+    fn complex_proximity_requirements() {
+        let a_site = Vec2 { x: 572, y: 724 };
+        let reqs = ProximityRequirements::new()
+            .close_to_one_of(vec![a_site].into_iter(), 60)
+            .avoid_all_of(vec![a_site].into_iter(), 40);
+        assert!(reqs.satisfied_by(Vec2 { x: 572, y: 774 }));
+        assert!(!reqs.satisfied_by(a_site));
+    }
+
+    #[test]
+    fn location_hint() {
+        let reqs = ProximityRequirements::new().close_to_one_of(
+            vec![Vec2 { x: 1, y: 0 }, Vec2 { x: 13, y: 12 }].into_iter(),
+            10,
+        );
+        let expected = Aabr {
+            min: Vec2 { x: 0, y: 0 },
+            max: Vec2 { x: 23, y: 22 },
+        };
+        let map_dims = Aabr {
+            min: Vec2 { x: 0, y: 0 },
+            max: Vec2 { x: 200, y: 300 },
+        };
+        assert_eq!(expected, reqs.location_hint(&map_dims));
+    }
 }