diff --git a/validation/src/check.rs b/validation/src/check.rs
index 56cf35b..5821148 100644
--- a/validation/src/check.rs
+++ b/validation/src/check.rs
@@ -40,25 +40,24 @@ impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
 	}
 }
 
-enum Zone{
-	Start(ModeID),
-	Finish(ModeID),
-	Anticheat(ModeID),
-}
-
 #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
 struct ModeID(u64);
 impl ModeID{
 	const MAIN:Self=Self(0);
 	const BONUS:Self=Self(1);
 }
+enum Zone{
+	Start(ModeID),
+	Finish(ModeID),
+	Anticheat(ModeID),
+}
 #[allow(dead_code)]
-pub enum ZoneParseError{
+pub enum IDParseError{
 	NoCaptures,
 	ParseInt(core::num::ParseIntError)
 }
 impl std::str::FromStr for Zone{
-	type Err=ZoneParseError;
+	type Err=IDParseError;
 	fn from_str(s:&str)->Result<Self,Self::Err>{
 		match s{
 			"MapStart"=>Ok(Self::Start(ModeID::MAIN)),
@@ -70,38 +69,78 @@ impl std::str::FromStr for Zone{
 			other=>{
 				let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
 				if let Some(captures)=bonus_start_pattern.captures(other){
-					return Ok(Self::Start(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?)));
+					return Ok(Self::Start(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
 				}
 				let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
 				if let Some(captures)=bonus_finish_pattern.captures(other){
-					return Ok(Self::Finish(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?)));
+					return Ok(Self::Finish(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
 				}
 				let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
 				if let Some(captures)=bonus_finish_pattern.captures(other){
-					return Ok(Self::Anticheat(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?)));
+					return Ok(Self::Anticheat(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
 				}
-				Err(ZoneParseError::NoCaptures)
+				Err(IDParseError::NoCaptures)
 			}
 		}
 	}
 }
 
-
-#[derive(Debug,Hash,Eq,PartialEq)]
+#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
 struct SpawnID(u64);
 impl SpawnID{
 	const FIRST:Self=Self(1);
 }
-#[derive(Debug,Hash,Eq,PartialEq)]
-struct WormholeOutID(u64);
+enum SpawnTeleport{
+	Teleport(SpawnID),
+	Spawn(SpawnID),
+}
+impl std::str::FromStr for SpawnTeleport{
+	type Err=IDParseError;
+	fn from_str(s:&str)->Result<Self,Self::Err>{
+		// Trigger ForceTrigger Teleport ForceTeleport SpawnAt ForceSpawnAt
+		let bonus_start_pattern=lazy_regex::lazy_regex!(r"^(?:Force)?(Teleport|SpawnAt|Trigger)(\d+)$");
+		if let Some(captures)=bonus_start_pattern.captures(s){
+			return Ok(Self::Teleport(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
+		}
+		// Spawn
+		let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
+		if let Some(captures)=bonus_finish_pattern.captures(s){
+			return Ok(Self::Spawn(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
+		}
+		Err(IDParseError::NoCaptures)
+	}
+}
+
+#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
+struct WormholeID(u64);
+enum Wormhole{
+	In(WormholeID),
+	Out(WormholeID),
+}
+impl std::str::FromStr for Wormhole{
+	type Err=IDParseError;
+	fn from_str(s:&str)->Result<Self,Self::Err>{
+		let bonus_start_pattern=lazy_regex::lazy_regex!(r"^WormholeIn(\d+)$");
+		if let Some(captures)=bonus_start_pattern.captures(s){
+			return Ok(Self::In(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
+		}
+		let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
+		if let Some(captures)=bonus_finish_pattern.captures(s){
+			return Ok(Self::Out(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?)));
+		}
+		Err(IDParseError::NoCaptures)
+	}
+}
 
 #[derive(Default)]
 struct Counts{
 	mode_start_counts:HashMap<ModeID,u64>,
 	mode_finish_counts:HashMap<ModeID,u64>,
 	mode_anticheat_counts:HashMap<ModeID,u64>,
+	teleport_counts:HashMap<SpawnID,u64>,
 	spawn_counts:HashMap<SpawnID,u64>,
-	wormhole_out_counts:HashMap<WormholeOutID,u64>,
+	wormhole_in_counts:HashMap<WormholeID,u64>,
+	wormhole_out_counts:HashMap<WormholeID,u64>,
 }
 
 pub struct ModelInfo<'a>{
@@ -126,19 +165,17 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d
 				Ok(Zone::Anticheat(mode_id))=>*counts.mode_anticheat_counts.entry(mode_id).or_insert(0)+=1,
 				Err(_)=>(),
 			}
-			// Spawns
-			let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
-			if let Some(captures)=spawn_pattern.captures(instance.name.as_str()){
-				if let Ok(spawn_id)=captures[1].parse(){
-					*counts.spawn_counts.entry(SpawnID(spawn_id)).or_insert(0)+=1;
-				}
+			// Spawns & Teleports
+			match instance.name.parse(){
+				Ok(SpawnTeleport::Teleport(spawn_id))=>*counts.teleport_counts.entry(spawn_id).or_insert(0)+=1,
+				Ok(SpawnTeleport::Spawn(spawn_id))=>*counts.spawn_counts.entry(spawn_id).or_insert(0)+=1,
+				Err(_)=>(),
 			}
-			// WormholeOuts
-			let wormhole_out_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
-			if let Some(captures)=wormhole_out_pattern.captures(instance.name.as_str()){
-				if let Ok(wormhole_out_id)=captures[1].parse(){
-					*counts.wormhole_out_counts.entry(WormholeOutID(wormhole_out_id)).or_insert(0)+=1;
-				}
+			// Wormholes
+			match instance.name.parse(){
+				Ok(Wormhole::In(wormhole_id))=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1,
+				Ok(Wormhole::Out(wormhole_id))=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1,
+				Err(_)=>(),
 			}
 		}
 	}
@@ -289,14 +326,19 @@ pub struct MapCheck<'a>{
 	mode_start_counts:DuplicateCheck<ModeID>,
 	// At least one finish zone for each start zone, and no finishes with no start
 	mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID>>,
-	// check for dangling MapAnticheat zones (no associated MapStart)
+	// Check for dangling MapAnticheat zones (no associated MapStart)
 	mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID>>,
 	// Spawn1 must exist
 	spawn1:Result<(),()>,
+	// Check for dangling Teleport# (no associated Spawn#)
+	teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID>>,
 	// No duplicate Spawn#
 	spawn_counts:DuplicateCheck<SpawnID>,
+	// Check for dangling WormholeIn# (no associated WormholeOut#)
+	wormhole_in_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<WormholeID>>,
 	// No duplicate WormholeOut# (duplicate WormholeIn# ok)
-	wormhole_out_counts:DuplicateCheck<WormholeOutID>,
+	// No dangling WormholeOut#
+	wormhole_out_counts:DuplicateCheck<WormholeID>,
 }
 
 impl<'a> ModelInfo<'a>{
@@ -354,9 +396,19 @@ impl<'a> ModelInfo<'a>{
 		// There must be exactly one start zone for every mode in the map.
 		let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check();
 
+		// Check that there are no Teleports without a corresponding Spawn.
+		// Spawns are allowed to have 0 Teleports.
+		let teleport_counts=SetDifferenceCheckContextAllowNone::new(self.counts.teleport_counts)
+			.check(&self.counts.spawn_counts);
+
 		// There must be exactly one of any perticular spawn id in the map.
 		let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check();
 
+		// Check that at least one WormholeIn exists for each WormholeOut.
+		// This also checks that there are no WormholeIn without a corresponding WormholeOut.
+		let wormhole_in_counts=SetDifferenceCheckContextAtLeastOne::new(self.counts.wormhole_in_counts)
+			.check(&self.counts.wormhole_out_counts);
+
 		// There must be exactly one of any perticular wormhole out id in the map.
 		let wormhole_out_counts=DuplicateCheckContext(self.counts.wormhole_out_counts).check();
 
@@ -371,7 +423,9 @@ impl<'a> ModelInfo<'a>{
 			mode_finish_counts,
 			mode_anticheat_counts,
 			spawn1,
+			teleport_counts,
 			spawn_counts,
+			wormhole_in_counts,
 			wormhole_out_counts,
 		}
 	}
@@ -391,7 +445,9 @@ impl<'a> MapCheck<'a>{
 				mode_finish_counts:SetDifferenceCheck(Ok(())),
 				mode_anticheat_counts:SetDifferenceCheck(Ok(())),
 				spawn1:Ok(()),
+				teleport_counts:SetDifferenceCheck(Ok(())),
 				spawn_counts:DuplicateCheck(Ok(())),
+				wormhole_in_counts:SetDifferenceCheck(Ok(())),
 				wormhole_out_counts:DuplicateCheck(Ok(())),
 			}=>{
 				Ok(MapInfoOwned{
@@ -498,6 +554,17 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
 		if let Err(())=&self.spawn1{
 			writeln!(f,"Model has no Spawn1")?;
 		}
+		if let SetDifferenceCheck(Err(context))=&self.teleport_counts{
+			if !context.extra.is_empty(){
+				// TODO: include original names of objects in hashmap value as Vec<&str>
+				let plural=if context.extra.len()==1{"object"}else{"objects"};
+				write!(f,"Extra Spawn-type {plural} with no matching Spawn: ")?;
+				write_comma_separated(f,context.extra.iter(),|f,(SpawnID(spawn_id),_count)|
+					write!(f,"Teleport or Trigger or SpawnAt #{spawn_id}")
+				)?;
+				writeln!(f,"")?;
+			}
+		}
 		if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{
 			write!(f,"Duplicate Spawn: ")?;
 			write_comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)|
@@ -505,9 +572,27 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
 			)?;
 			writeln!(f,"")?;
 		}
+		if let SetDifferenceCheck(Err(context))=&self.wormhole_in_counts{
+			if !context.extra.is_empty(){
+				write!(f,"WormholeIn with no matching WormholeOut: ")?;
+				write_comma_separated(f,context.extra.iter(),|f,(WormholeID(wormhole_id),_count)|
+					write!(f,"WormholeIn{wormhole_id}")
+				)?;
+				writeln!(f,"")?;
+			}
+			if !context.missing.is_empty(){
+				// This counts WormholeIn objects, but
+				// flipped logic is easier to understand
+				write!(f,"WormholeOut with no matching WormholeIn: ")?;
+				write_comma_separated(f,context.missing.iter(),|f,WormholeID(wormhole_id)|
+					write!(f,"WormholeOut{wormhole_id}")
+				)?;
+				writeln!(f,"")?;
+			}
+		}
 		if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{
 			write!(f,"Duplicate WormholeOut: ")?;
-			write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_id),count)|
+			write_comma_separated(f,context.iter(),|f,(WormholeID(wormhole_id),count)|
 				write!(f,"WormholeOut{wormhole_id}({count} duplicates)")
 			)?;
 			writeln!(f,"")?;