-- 015: 어구 모선 추론 episode continuity + prior bonus SET search_path TO kcg, public; ALTER TABLE kcg.gear_group_parent_candidate_snapshots ADD COLUMN IF NOT EXISTS normalized_parent_name VARCHAR(200); ALTER TABLE kcg.gear_group_parent_candidate_snapshots ADD COLUMN IF NOT EXISTS episode_id VARCHAR(64); ALTER TABLE kcg.gear_group_parent_candidate_snapshots ADD COLUMN IF NOT EXISTS episode_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0; ALTER TABLE kcg.gear_group_parent_candidate_snapshots ADD COLUMN IF NOT EXISTS lineage_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0; ALTER TABLE kcg.gear_group_parent_candidate_snapshots ADD COLUMN IF NOT EXISTS label_prior_bonus DOUBLE PRECISION NOT NULL DEFAULT 0; UPDATE kcg.gear_group_parent_candidate_snapshots SET normalized_parent_name = regexp_replace(upper(COALESCE(parent_name, '')), '[[:space:]_%-]+', '', 'g') WHERE normalized_parent_name IS NULL; CREATE INDEX IF NOT EXISTS idx_ggpcs_episode_time ON kcg.gear_group_parent_candidate_snapshots(episode_id, observed_at DESC); CREATE INDEX IF NOT EXISTS idx_ggpcs_lineage_time ON kcg.gear_group_parent_candidate_snapshots(normalized_parent_name, observed_at DESC); ALTER TABLE kcg.gear_group_parent_resolution ADD COLUMN IF NOT EXISTS episode_id VARCHAR(64); ALTER TABLE kcg.gear_group_parent_resolution ADD COLUMN IF NOT EXISTS continuity_source VARCHAR(32); ALTER TABLE kcg.gear_group_parent_resolution ADD COLUMN IF NOT EXISTS continuity_score DOUBLE PRECISION; ALTER TABLE kcg.gear_group_parent_resolution ADD COLUMN IF NOT EXISTS prior_bonus_total DOUBLE PRECISION NOT NULL DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_ggpr_episode ON kcg.gear_group_parent_resolution(episode_id); ALTER TABLE kcg.gear_parent_label_sessions ADD COLUMN IF NOT EXISTS normalized_parent_name VARCHAR(200); UPDATE kcg.gear_parent_label_sessions SET normalized_parent_name = regexp_replace(upper(COALESCE(group_key, '')), '[[:space:]_%-]+', '', 'g') WHERE normalized_parent_name IS NULL; CREATE INDEX IF NOT EXISTS idx_gpls_lineage_active ON kcg.gear_parent_label_sessions(normalized_parent_name, active_from DESC); CREATE TABLE IF NOT EXISTS kcg.gear_group_episodes ( episode_id VARCHAR(64) PRIMARY KEY, lineage_key VARCHAR(200) NOT NULL, group_key VARCHAR(100) NOT NULL, normalized_parent_name VARCHAR(200) NOT NULL, current_sub_cluster_id SMALLINT NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', continuity_source VARCHAR(32) NOT NULL DEFAULT 'NEW', continuity_score DOUBLE PRECISION, first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_snapshot_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), current_member_count INT NOT NULL DEFAULT 0, current_member_mmsis JSONB NOT NULL DEFAULT '[]'::jsonb, current_center_point geometry(Point, 4326), split_from_episode_id VARCHAR(64), merged_from_episode_ids JSONB NOT NULL DEFAULT '[]'::jsonb, merged_into_episode_id VARCHAR(64), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_gge_status CHECK (status IN ('ACTIVE', 'MERGED', 'EXPIRED')), CONSTRAINT chk_gge_continuity CHECK (continuity_source IN ('NEW', 'CONTINUED', 'SPLIT_CONTINUE', 'SPLIT_NEW', 'MERGE_NEW', 'DIRECT_PARENT_MATCH')) ); CREATE INDEX IF NOT EXISTS idx_gge_lineage_status_time ON kcg.gear_group_episodes(lineage_key, status, last_snapshot_time DESC); CREATE INDEX IF NOT EXISTS idx_gge_group_time ON kcg.gear_group_episodes(group_key, current_sub_cluster_id, last_snapshot_time DESC); CREATE TABLE IF NOT EXISTS kcg.gear_group_episode_snapshots ( id BIGSERIAL PRIMARY KEY, episode_id VARCHAR(64) NOT NULL REFERENCES kcg.gear_group_episodes(episode_id) ON DELETE CASCADE, lineage_key VARCHAR(200) NOT NULL, group_key VARCHAR(100) NOT NULL, normalized_parent_name VARCHAR(200) NOT NULL, sub_cluster_id SMALLINT NOT NULL DEFAULT 0, observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), member_count INT NOT NULL DEFAULT 0, member_mmsis JSONB NOT NULL DEFAULT '[]'::jsonb, center_point geometry(Point, 4326), continuity_source VARCHAR(32) NOT NULL, continuity_score DOUBLE PRECISION, parent_episode_ids JSONB NOT NULL DEFAULT '[]'::jsonb, top_candidate_mmsi VARCHAR(20), top_candidate_score DOUBLE PRECISION, resolution_status VARCHAR(40), metadata JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT uq_gges_episode_observed UNIQUE (episode_id, observed_at) ); CREATE INDEX IF NOT EXISTS idx_gges_lineage_observed ON kcg.gear_group_episode_snapshots(lineage_key, observed_at DESC); CREATE INDEX IF NOT EXISTS idx_gges_group_observed ON kcg.gear_group_episode_snapshots(group_key, sub_cluster_id, observed_at DESC);