-- Verification queries for the priority-states scrape batches. -- -- Run via psql against Neon dev (NEON_DATABASE_URL_DEV): -- ./packages/hosting/scripts/neon/psql_resolved.sh -f scripts/datasources/jurisdiction_pilot/verify.sql -- Or pass a specific batch: -- psql "$NEON_DATABASE_URL_DEV" \ -- -v batch="''" \ -- -f scripts/datasources/jurisdiction_pilot/verify.sql \if :{?batch} \else \set batch (SELECT scrape_batch_id::text FROM ( \ SELECT scrape_batch_id, MAX(loaded_at) AS t FROM bronze.bronze_contacts_scraped GROUP BY 1 \ UNION ALL \ SELECT scrape_batch_id, MAX(loaded_at) AS t FROM intermediate.int_events_channels_candidates GROUP BY 1 \ ) x ORDER BY t DESC LIMIT 1) \endif \echo \echo === Batch in scope === SELECT :batch::uuid AS batch_id; \echo \echo === Contacts per state === SELECT state_code, COUNT(DISTINCT jurisdiction_id) AS jurisdictions_with_contacts, COUNT(*) AS contact_rows, SUM((raw_row->>'is_mayor')::boolean::int) AS mayor_rows, SUM(CASE WHEN email IS NOT NULL THEN 1 END) AS rows_with_email, SUM(CASE WHEN phone IS NOT NULL THEN 1 END) AS rows_with_phone FROM bronze.bronze_contacts_scraped WHERE scrape_batch_id = :batch::uuid GROUP BY state_code ORDER BY state_code; \echo \echo === YouTube: candidates vs verified (this batch) === SELECT 'candidates' AS layer, state_code, COUNT(DISTINCT jurisdiction_id) AS jurisdictions, COUNT(*) AS channel_rows, COUNT(*) FILTER (WHERE is_verified) AS verified_flagged, COUNT(*) FILTER (WHERE NOT is_verified) AS rejected, COUNT(DISTINCT rejection_reason) AS distinct_rejection_reasons FROM intermediate.int_events_channels_candidates WHERE scrape_batch_id = :batch::uuid GROUP BY state_code UNION ALL SELECT 'verified_canonical' AS layer, state_code, COUNT(DISTINCT jurisdiction_id), COUNT(*), COUNT(*) FILTER (WHERE is_primary), NULL::bigint, NULL::bigint FROM intermediate.int_events_channels WHERE scrape_batch_id = :batch::uuid GROUP BY state_code ORDER BY layer, state_code; \echo \echo === Rejected candidate reasons (top) === SELECT COALESCE(rejection_reason, '(verified)') AS reason, COUNT(*) AS n FROM intermediate.int_events_channels_candidates WHERE scrape_batch_id = :batch::uuid GROUP BY 1 ORDER BY n DESC LIMIT 20; \echo \echo === Verified YouTube channel purpose breakdown === SELECT COALESCE(channel_purpose, 'unknown') AS channel_purpose, COUNT(*) AS rows, COUNT(*) FILTER (WHERE is_primary) AS primary_rows FROM intermediate.int_events_channels GROUP BY 1 ORDER BY rows DESC; \echo \echo === Non-meeting channels still in verified (should review) === SELECT jurisdiction_id, channel_title, channel_purpose, official_meeting_confidence FROM intermediate.int_events_channels WHERE channel_purpose IN ('county-general', 'municipality-general', 'tv-public', 'unknown') ORDER BY channel_purpose, jurisdiction_id LIMIT 30; \echo \echo === Verified YouTube metadata coverage === SELECT COUNT(*) AS rows, COUNT(*) FILTER (WHERE channel_description IS NOT NULL AND BTRIM(channel_description) <> '') AS has_description, COUNT(*) FILTER (WHERE subscriber_count IS NOT NULL) AS has_subscribers, COUNT(*) FILTER (WHERE video_count IS NOT NULL) AS has_video_count, COUNT(*) FILTER (WHERE latest_upload IS NOT NULL AND BTRIM(latest_upload) <> '') AS has_latest_upload, COUNT(*) FILTER (WHERE jsonb_array_length(jurisdiction_website_back_links) > 0) AS has_back_links FROM intermediate.int_events_channels; \echo \echo === City channels incorrectly on counties (should be 0 verified) === SELECT y.jurisdiction_id AS county_id, j.name AS county_name, y.channel_title, y.youtube_channel_url FROM intermediate.int_events_channels y JOIN intermediate.int_jurisdictions j ON j.jurisdiction_id = y.jurisdiction_id WHERE y.jurisdiction_type = 'county' AND ( LOWER(BTRIM(COALESCE(y.channel_title, ''))) ~ '^city of ' OR POSITION('cityof' IN LOWER(y.youtube_channel_url)) > 0 ) ORDER BY y.state_code, y.jurisdiction_id LIMIT 25; \echo \echo === YouTube metadata quality (junk titles / missing description / back-links) === SELECT 'verified' AS layer, COUNT(*) AS rows, COUNT(*) FILTER ( WHERE LOWER(BTRIM(channel_title)) IN ('home','videos','shorts','live','playlists','community','about') ) AS junk_tab_titles, COUNT(*) FILTER ( WHERE channel_description IS NULL OR BTRIM(channel_description) = '' ) AS missing_description, COUNT(*) FILTER ( WHERE jsonb_array_length(jurisdiction_website_back_links) > 0 ) AS has_jurisdiction_back_links FROM intermediate.int_events_channels UNION ALL SELECT 'candidates', COUNT(*), COUNT(*) FILTER ( WHERE LOWER(BTRIM(channel_title)) IN ('home','videos','shorts','live','playlists','community','about') ), COUNT(*) FILTER ( WHERE channel_description IS NULL OR BTRIM(channel_description) = '' ), COUNT(*) FILTER ( WHERE jsonb_array_length(jurisdiction_website_back_links) > 0 ) FROM intermediate.int_events_channels_candidates; \echo \echo === Rows with junk tab titles (sample) === SELECT 'verified' AS layer, jurisdiction_id, channel_title, LEFT(channel_description, 80) AS description_preview, jurisdiction_website_back_links, youtube_channel_url FROM intermediate.int_events_channels WHERE LOWER(BTRIM(channel_title)) IN ('home','videos','shorts','live','playlists','community','about') UNION ALL SELECT 'candidates', jurisdiction_id, channel_title, LEFT(channel_description, 80), jurisdiction_website_back_links, youtube_channel_url FROM intermediate.int_events_channels_candidates WHERE LOWER(BTRIM(channel_title)) IN ('home','videos','shorts','live','playlists','community','about') ORDER BY layer, jurisdiction_id LIMIT 25; \echo \echo === Verified canonical channels (GA counties sample) === SELECT y.jurisdiction_id, y.jurisdiction_type, j.name, y.channel_title, y.youtube_channel_url, y.official_meeting_confidence, y.source, y.is_primary, y.back_links_to_jurisdiction_website, y.jurisdiction_website_back_links FROM intermediate.int_events_channels y JOIN intermediate.int_jurisdictions j USING (jurisdiction_id) WHERE j.state_code = 'GA' AND y.jurisdiction_type = 'county' ORDER BY y.jurisdiction_id, y.is_primary DESC, y.official_meeting_confidence DESC NULLS LAST LIMIT 30; \echo \echo === Noise still in candidates only (pattern_match rejects) === SELECT c.jurisdiction_id, c.jurisdiction_type, j.name, c.channel_title, c.youtube_channel_url, c.official_meeting_confidence, c.rejection_reason FROM intermediate.int_events_channels_candidates c JOIN intermediate.int_jurisdictions j USING (jurisdiction_id) WHERE c.scrape_batch_id = :batch::uuid AND NOT c.is_verified ORDER BY c.jurisdiction_id LIMIT 30; \echo \echo === Mayor-flagged contacts (top 50) === SELECT state_code, jurisdiction_id, person_name, title_or_role, email, phone, source_page_url FROM bronze.bronze_contacts_scraped WHERE scrape_batch_id = :batch::uuid AND (raw_row->>'is_mayor')::boolean = TRUE ORDER BY state_code, jurisdiction_id LIMIT 50; \echo \echo === Coverage gap: jurisdictions with website but NO verified YouTube === WITH targets AS ( SELECT DISTINCT ON (jurisdiction_id) jurisdiction_id, state_code, COALESCE(NULLIF(btrim(j.name), ''), jurisdiction_id) AS name, btrim(website_url) AS website_url FROM intermediate.int_jurisdiction_websites w LEFT JOIN intermediate.int_jurisdictions j USING (jurisdiction_id) WHERE w.state_code IN ('AL','GA','IN','MA','WA','WI') AND w.jurisdiction_category IN ('municipality','county') AND w.website_url IS NOT NULL AND btrim(w.website_url) <> '' ORDER BY jurisdiction_id, w.website_record_key ), verified AS ( SELECT DISTINCT jurisdiction_id FROM intermediate.int_events_channels ) SELECT t.state_code, t.jurisdiction_id, t.name, t.website_url FROM targets t LEFT JOIN verified v USING (jurisdiction_id) WHERE v.jurisdiction_id IS NULL ORDER BY t.state_code, t.jurisdiction_id LIMIT 50; \echo \echo === Checkpoint file === \echo (See data/bronze/jurisdiction_pilot_progress/.jsonl)