{
  "sha": "08e6974a05f3704253ae1e0bfe54edadebb0678f",
  "message": "improve airline image quality profiles",
  "author": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "committer": {
    "login": "nnneeeooo-nnneeeooo",
    "name": "",
    "email": ""
  },
  "created_at": "2026-09-10T14:09:47Z",
  "files": [
    {
      "filename": "config/airline_visual_profiles.json",
      "previous_filename": "",
      "status": "added",
      "additions": 96,
      "deletions": 0,
      "changes": 96
    },
    {
      "filename": "data/articles/2026-07-28T17.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 7,
      "deletions": 7,
      "changes": 14
    },
    {
      "filename": "data/articles/2026-08-06T12.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 6,
      "deletions": 6,
      "changes": 12
    },
    {
      "filename": "data/articles/2026-08-20T07.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 4,
      "changes": 8
    },
    {
      "filename": "data/articles/2026-08-20T11.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 4,
      "changes": 8
    },
    {
      "filename": "data/articles/2026-08-26T12.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 7,
      "deletions": 7,
      "changes": 14
    },
    {
      "filename": "data/articles/2026-09-10T07.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 6,
      "deletions": 6,
      "changes": 12
    },
    {
      "filename": "data/images.json",
      "previous_filename": "",
      "status": "modified",
      "additions": 61,
      "deletions": 34,
      "changes": 95
    },
    {
      "filename": "pipeline/image_fallbacks.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 0,
      "deletions": 18,
      "changes": 18
    },
    {
      "filename": "pipeline/image_selection.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 5,
      "deletions": 0,
      "changes": 5
    },
    {
      "filename": "pipeline/images.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 110,
      "deletions": 8,
      "changes": 118
    },
    {
      "filename": "tests/test_image_fallbacks.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 4,
      "deletions": 8,
      "changes": 12
    },
    {
      "filename": "tests/test_image_selection.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 90,
      "deletions": 0,
      "changes": 90
    },
    {
      "filename": "tests/test_images.py",
      "previous_filename": "",
      "status": "modified",
      "additions": 3,
      "deletions": 2,
      "changes": 5
    }
  ],
  "diff": "@@ -0,0 +1,96 @@\\n+{\\n+  \"version\": 1,\\n+  \"description\": \"Verified fleet-role preferences for generic airline news images. Exact event, registration and article-named aircraft evidence always outrank these profiles.\",\\n+  \"profiles\": [\\n+    {\\n+      \"airline\": \"China Airlines\",\\n+      \"flagship\": [\"Airbus A350-900\", \"Boeing 777-300ER\"],\\n+      \"regional\": [\"Airbus A321neo\", \"Airbus A330-300\"],\\n+      \"cargo\": [\"Boeing 777F\", \"Boeing 747-400F\"],\\n+      \"future\": [\"Airbus A350-1000\", \"Boeing 787-9\", \"Boeing 787-10\", \"Boeing 777-9\", \"Boeing 777-8F\"],\\n+      \"excluded_generic\": [\"Boeing 737-800\"],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.china-airlines.com/tw/en/about-china-airlines/about-us/operations\"\\n+    },\\n+    {\\n+      \"airline\": \"EVA Air\",\\n+      \"flagship\": [\"Boeing 787-10\", \"Boeing 787-9\", \"Boeing 777-300ER\"],\\n+      \"regional\": [\"Boeing 787-10\", \"Airbus A330-300\", \"Airbus A321-200\"],\\n+      \"cargo\": [\"Boeing 777F\"],\\n+      \"future\": [\"Airbus A350-1000\", \"Airbus A321neo\"],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.evaair.com/en-tw/about-eva-air/about-us/market-and-sales-overview/eva-air-fleet/\"\\n+    },\\n+    {\\n+      \"airline\": \"STARLUX Airlines\",\\n+      \"flagship\": [\"Airbus A350-1000\", \"Airbus A350-900\"],\\n+      \"regional\": [\"Airbus A330neo\", \"Airbus A321neo\"],\\n+      \"cargo\": [],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.starlux-airlines.com/en-US/experience/walk-into-starlux/our-fleet\"\\n+    },\\n+    {\\n+      \"airline\": \"Delta Air Lines\",\\n+      \"flagship\": [\"Airbus A350-900\", \"Airbus A330neo\"],\\n+      \"regional\": [\"Airbus A321neo\", \"Airbus A220-300\"],\\n+      \"cargo\": [],\\n+      \"future\": [\"Airbus A350-1000\", \"Boeing 787-10\"],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.delta.com/us/en/aircraft/overview\"\\n+    },\\n+    {\\n+      \"airline\": \"American Airlines\",\\n+      \"flagship\": [\"Boeing 787-9\", \"Boeing 777-300ER\"],\\n+      \"regional\": [\"Airbus A321neo\", \"Boeing 737 MAX 8\"],\\n+      \"cargo\": [],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.aa.com/web/i18n/travel-info/experience/planes/planes.html\"\\n+    },\\n+    {\\n+      \"airline\": \"Emirates\",\\n+      \"flagship\": [\"Airbus A380-800\", \"Airbus A350-900\", \"Boeing 777-300ER\"],\\n+      \"regional\": [\"Airbus A350-900\", \"Boeing 777-300ER\"],\\n+      \"cargo\": [],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.emirates.com/us/english/experience/our-fleet/\"\\n+    },\\n+    {\\n+      \"airline\": \"British Airways\",\\n+      \"flagship\": [\"Airbus A350-1000\", \"Boeing 787-10\", \"Boeing 787-9\"],\\n+      \"regional\": [\"Airbus A321neo\", \"Airbus A320neo\"],\\n+      \"cargo\": [],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.britishairways.com/content/information/about-ba/fleet-facts\"\\n+    },\\n+    {\\n+      \"airline\": \"All Nippon Airways\",\\n+      \"flagship\": [\"Boeing 787-9\", \"Boeing 787-10\"],\\n+      \"regional\": [\"Airbus A321neo\", \"Airbus A320neo\"],\\n+      \"cargo\": [\"Boeing 777F\", \"Boeing 767-300F\"],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.ana.co.jp/group/en/company/ana/scale/\"\\n+    },\\n+    {\\n+      \"airline\": \"Qatar Airways\",\\n+      \"flagship\": [\"Airbus A350-1000\", \"Airbus A350-900\", \"Boeing 787-9\"],\\n+      \"regional\": [\"Boeing 787-9\", \"Airbus A320-200\"],\\n+      \"cargo\": [],\\n+      \"future\": [],\\n+      \"excluded_generic\": [],\\n+      \"verified_utc\": \"2026-09-10T00:00:00Z\",\\n+      \"source\": \"https://www.qatarairways.com/en-gb/fleet.html\"\\n+    }\\n+  ]\\n+}\\n@@ -143,15 +143,15 @@\\n         ]\\n       },\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187945993\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg\",\\n+        \"credit\": \"Melv_L - MACASR\",\\n+        \"license\": \"CC BY-SA 2.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"China Airlines aircraft\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n-        \"photoYear\": 2026\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n+        \"photoYear\": 2019\\n       }\\n     }\\n   ]\\n@@ -6,14 +6,14 @@\\n       \"cat\": \"ops\",\\n       \"primarySource\": \"中央社 CNA 國籍航空\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187945993\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg\",\\n+        \"credit\": \"Alan Wilson\",\\n+        \"license\": \"CC BY-SA 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"China Airlines aircraft\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n         \"photoYear\": 2026\\n       },\\n       \"zh\": {\\n@@ -106,14 +106,14 @@\\n         ]\\n       },\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a4/China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg\",\\n         \"credit\": \"4300streetcar\",\\n         \"license\": \"CC BY 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"topic:china-airlines\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Boeing 777F\",\\n+        \"subject\": \"China Airlines Cargo Boeing 777F\",\\n         \"photoYear\": 2026\\n       }\\n     },\\n@@ -89,14 +89,14 @@\\n         ]\\n       },\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n+        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187535326\",\\n         \"credit\": \"4300streetcar\",\\n         \"license\": \"CC BY 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"topic:china-airlines\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Boeing 777-300ER\",\\n+        \"subject\": \"China Airlines Boeing 777-300ER\",\\n         \"photoYear\": 2026\\n       }\\n     },\\n@@ -6,15 +6,15 @@\\n       \"cat\": \"ops\",\\n       \"primarySource\": \"中央社 CNA 國籍航空\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg\",\\n+        \"credit\": \"Melv_L - MACASR\",\\n+        \"license\": \"CC BY-SA 2.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"topic:china-airlines\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n-        \"photoYear\": 2026\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n+        \"photoYear\": 2019\\n       },\\n       \"zh\": {\\n         \"title\": \"華航預計2027年3月起調整一線人員名牌呈現方式\",\\n@@ -6,14 +6,14 @@\\n       \"cat\": \"biz\",\\n       \"primarySource\": \"中央社 CNA 國籍航空\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg\",\\n+        \"credit\": \"Alan Wilson\",\\n+        \"license\": \"CC BY-SA 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"topic:china-airlines\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n         \"photoYear\": 2026\\n       },\\n       \"sourceImageCandidates\": [],\\n@@ -513,17 +513,17 @@\\n     \"a-20260806-1215-typhoon-white-dolphin-prompts-china-airl\": {\\n       \"status\": \"matched\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187945993\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg\",\\n+        \"credit\": \"Alan Wilson\",\\n+        \"license\": \"CC BY-SA 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"China Airlines aircraft\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n         \"photoYear\": 2026\\n       },\\n-      \"checked_utc\": \"2026-08-06T13:56:31.252671+00:00\"\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260806-1215-china-airlines-airlifts-relief-partition\": {\\n       \"status\": \"matched\",\\n@@ -739,17 +739,17 @@\\n     \"a-20260728-1735-kumamoto-earthquake-taiwan-flight-disruptions\": {\\n       \"status\": \"matched\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187945993\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg\",\\n+        \"credit\": \"Melv_L - MACASR\",\\n+        \"license\": \"CC BY-SA 2.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"China Airlines aircraft\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n-        \"photoYear\": 2026\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n+        \"photoYear\": 2019\\n       },\\n-      \"checked_utc\": \"2026-08-09T20:58:03.582807+00:00\"\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260808-0704-japan-airlines-and-malaysia-airlines-lau\": {\\n       \"status\": \"matched\",\\n@@ -2308,16 +2308,34 @@\\n       \"checked_utc\": \"2026-08-24T10:31:26+00:00\"\\n     },\\n     \"a-20260820-1119-china-airlines-to-launch-new-business-cl\": {\\n-      \"status\": \"none\",\\n-      \"attempts\": 3,\\n-      \"checked_utc\": \"2026-08-25T01:11:41.505073+00:00\",\\n-      \"next_retry_utc\": \"2026-08-25T07:11:41.505073+00:00\"\\n+      \"status\": \"matched\",\\n+      \"image\": {\\n+        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_777-300ER_B-18051_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/w/index.php?curid=187535326\",\\n+        \"credit\": \"4300streetcar\",\\n+        \"license\": \"CC BY 4.0\",\\n+        \"provider\": \"Wikimedia Commons\",\\n+        \"kind\": \"file_photo\",\\n+        \"matched\": \"China Airlines Boeing 777-300ER\",\\n+        \"subject\": \"China Airlines Boeing 777-300ER\",\\n+        \"photoYear\": 2026\\n+      },\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260820-0733-china-airlines-to-raise-cargo-fuel-surch\": {\\n-      \"status\": \"none\",\\n-      \"attempts\": 3,\\n-      \"checked_utc\": \"2026-08-25T01:11:41.505073+00:00\",\\n-      \"next_retry_utc\": \"2026-08-25T07:11:41.505073+00:00\"\\n+      \"status\": \"matched\",\\n+      \"image\": {\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/a/a4/China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg/1280px-China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Cargo_Boeing_777F_B-18773_departing_Taoyuan_February_2026.jpg\",\\n+        \"credit\": \"4300streetcar\",\\n+        \"license\": \"CC BY 4.0\",\\n+        \"provider\": \"Wikimedia Commons\",\\n+        \"kind\": \"file_photo\",\\n+        \"matched\": \"China Airlines Boeing 777F\",\\n+        \"subject\": \"China Airlines Cargo Boeing 777F\",\\n+        \"photoYear\": 2026\\n+      },\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260817-0444-china-airlines-wins-best-companies-to-wo\": {\\n       \"status\": \"none\",\\n@@ -2814,10 +2832,19 @@\\n       \"checked_utc\": \"2026-08-26T10:29:25.961860+00:00\"\\n     },\\n     \"a-20260826-1232-china-airlines-to-revise-frontline-staff\": {\\n-      \"status\": \"none\",\\n-      \"attempts\": 1,\\n-      \"checked_utc\": \"2026-08-26T13:00:34.211320+00:00\",\\n-      \"next_retry_utc\": \"2026-08-26T19:00:34.211320+00:00\"\\n+      \"status\": \"matched\",\\n+      \"image\": {\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/c/c6/China_Airlines_Airbus_A350-900_B-18918.jpg/1280px-China_Airlines_Airbus_A350-900_B-18918.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Airbus_A350-900_B-18918.jpg\",\\n+        \"credit\": \"Melv_L - MACASR\",\\n+        \"license\": \"CC BY-SA 2.0\",\\n+        \"provider\": \"Wikimedia Commons\",\\n+        \"kind\": \"file_photo\",\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n+        \"photoYear\": 2019\\n+      },\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260826-1232-us-army-mh-60-black-hawk-comes-down-near\": {\\n       \"status\": \"matched\",\\n@@ -4577,17 +4604,17 @@\\n     \"a-20260910-0755-china-airlines-to-raise-international-bo\": {\\n       \"status\": \"matched\",\\n       \"image\": {\\n-        \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-        \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n-        \"credit\": \"4300streetcar\",\\n-        \"license\": \"CC BY 4.0\",\\n+        \"url\": \"https://thumb.wikimedia.org/wikipedia/commons/thumb/8/87/Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg/1280px-Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_%28Carbon_Fibre_livery%29_%2855500680951%29.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n+        \"link\": \"https://commons.wikimedia.org/wiki/File:Airbus_A350-941_%E2%80%98B-18918%E2%80%99_China_Airlines_(Carbon_Fibre_livery)_(55500680951).jpg\",\\n+        \"credit\": \"Alan Wilson\",\\n+        \"license\": \"CC BY-SA 4.0\",\\n         \"provider\": \"Wikimedia Commons\",\\n         \"kind\": \"file_photo\",\\n-        \"matched\": \"topic:china-airlines\",\\n-        \"subject\": \"China Airlines Boeing 737-800\",\\n+        \"matched\": \"China Airlines Airbus A350-900\",\\n+        \"subject\": \"China Airlines Airbus A350-900\",\\n         \"photoYear\": 2026\\n       },\\n-      \"checked_utc\": \"2026-09-10T08:16:48.824123+00:00\"\\n+      \"checked_utc\": \"2026-09-10T14:04:56+00:00\"\\n     },\\n     \"a-20260910-0755-netherlands-signs-letter-of-intent-to-ac\": {\\n       \"status\": \"none\",\\n@@ -79,24 +79,6 @@\\n             \"photoYear\": 2012,\\n         },\\n     },\\n-    {\\n-        \"name\": \"china-airlines\",\\n-        \"patterns\": (\\n-            re.compile(r\"^(?:華航|中華航空)\"),\\n-            re.compile(r\"^China Airlines\\b\", re.I),\\n-        ),\\n-        \"image\": {\\n-            \"url\": \"https://upload.wikimedia.org/wikipedia/commons/thumb/d/df/China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg/1280px-China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg?utm_source=commons.wikimedia.org&utm_campaign=imageinfo&utm_content=thumbnail\",\\n-            \"link\": \"https://commons.wikimedia.org/wiki/File:China_Airlines_Boeing_737-800_B-18651_on_final_approach_to_Taoyuan_February_2026.jpg\",\\n-            \"credit\": \"4300streetcar\",\\n-            \"license\": \"CC BY 4.0\",\\n-            \"provider\": \"Wikimedia Commons\",\\n-            \"kind\": \"file_photo\",\\n-            \"matched\": \"topic:china-airlines\",\\n-            \"subject\": \"China Airlines Boeing 737-800\",\\n-            \"photoYear\": 2026,\\n-        },\\n-    },\\n     {\\n         \"name\": \"f16\",\\n         \"patterns\": (\\n@@ -222,6 +222,11 @@ def rejection_reason(article, raw, captions=None):\\n     if airline:\\n         if not any(phrase(alias, ev) for alias in images._airline_aliases(airline)):\\n             return \"airline-mismatch\"\\n+        if stock and not model:\\n+            quality_reason = images.profiled_airline_stock_reason(\\n+                article, airline, ev)\\n+            if quality_reason:\\n+                return quality_reason\\n         if not model and im.get(\"provider\") == \"Wikimedia Commons\":\\n             try:\\n                 if not images._article_year(article)-10 <= int(im.get(\"photoYear\")) <= images._article_year(article):\\n@@ -84,6 +84,7 @@\\n MAX_ARTICLE_AGE_DAYS = 7  # scan only the user-facing seven-day news window\\n GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS = 10\\n GENERIC_ORG_MAX_PHOTO_AGE_YEARS = 10\\n+MAX_PROFILE_MODEL_QUERIES = 2\\n \\n \\n def _retry_after_hours(attempts: int) -> int:\\n@@ -183,6 +184,19 @@ def _retry_after_hours(attempts: int) -> int:\\n                 .get(\"airports\") or [])\\n     if isinstance(a, dict)\\n ]\\n+_visual_profiles = {\\n+    str(row.get(\"airline\") or \"\").casefold(): row\\n+    for row in (load_json(ROOT / \"config\" / \"airline_visual_profiles.json\", {})\\n+                .get(\"profiles\") or [])\\n+    if isinstance(row, dict) and row.get(\"airline\")\\n+}\\n+\\n+_CARGO_VISUAL_RE = re.compile(\\n+    r\"貨機|貨運|航空貨運|全貨機|客改貨|\\bcargo\\b|\\bfreight(?:er)?\\b\", re.I)\\n+_REGIONAL_VISUAL_RE = re.compile(\\n+    r\"國內線|區域航線|短程航線|離島航線|\\bdomestic\\b|\\bregional\\b|\\bshort[- ]haul\\b\",\\n+    re.I,\\n+)\\n \\n \\n def _article_text(article: dict) -> str:\\n@@ -204,6 +218,58 @@ def _article_text(article: dict) -> str:\\n     return \" \".join(parts)\\n \\n \\n+def airline_visual_profile(airline: str | None) -> dict | None:\\n+    \"\"\"Return a verified profile; absence must never be filled by guessing.\"\"\"\\n+    return _visual_profiles.get(str(airline or \"\").casefold())\\n+\\n+\\n+def airline_visual_role(article: dict) -> str:\\n+    \"\"\"Classify only broad roles that are safe for a generic airline photo.\"\"\"\\n+    lead = f\"{article_headline_text(article)} {article_lead_text(article)}\"\\n+    if _CARGO_VISUAL_RE.search(lead):\\n+        return \"cargo\"\\n+    if _REGIONAL_VISUAL_RE.search(lead):\\n+        return \"regional\"\\n+    return \"flagship\"\\n+\\n+\\n+def preferred_airline_models(article: dict, airline: str | None) -> list[str]:\\n+    \"\"\"Verified representative models for an airline-only story.\\n+\\n+    Exact article-named models bypass this function.  A cargo story with no\\n+    verified cargo profile deliberately returns no passenger substitute.\\n+    \"\"\"\\n+    profile = airline_visual_profile(airline)\\n+    if not profile:\\n+        return []\\n+    role = airline_visual_role(article)\\n+    values = profile.get(role) or (\\n+        [] if role == \"cargo\" else profile.get(\"flagship\"))\\n+    return [str(value).strip() for value in values or [] if str(value).strip()]\\n+\\n+\\n+def profiled_airline_stock_reason(article: dict, airline: str,\\n+                                  evidence_text: str) -> str | None:\\n+    \"\"\"Reject a generic stock photo outside the airline's verified role.\"\"\"\\n+    if find_aircraft_type(article):\\n+        return None\\n+    profile = airline_visual_profile(airline)\\n+    if not profile:\\n+        return None\\n+    from image_selection import model_matches\\n+    role = airline_visual_role(article)\\n+    allowed = preferred_airline_models(article, airline)\\n+    if role == \"cargo\" and not allowed:\\n+        return \"airline-cargo-profile-unavailable\"\\n+    if any(model_matches(str(model), evidence_text)\\n+           for model in profile.get(\"excluded_generic\") or []):\\n+        return \"nonrepresentative-airline-stock\"\\n+    if allowed and not any(model_matches(model, evidence_text)\\n+                           for model in allowed):\\n+        return \"airline-role-mismatch\"\\n+    return None\\n+\\n+\\n def find_registration(article: dict) -> str | None:\\n     lead = article_lead_text(article)\\n     entities = article.get(\"entities\") or {}\\n@@ -552,7 +618,8 @@ def lookup_commons(query: str, require_tokens: list[str],\\n                    subject: str | None = None,\\n                    require_all: bool = False,\\n                    min_year: int | None = None,\\n-                   prefer_recent: bool = False,\\n+                   prefer_recent: bool = False, prefer_landscape: bool = False,\\n+                   required_model: str | None = None,\\n                    reject_title_re=None, article=None, usage=None) -> dict | None:\\n     \"\"\"First freely-licensed Commons bitmap matching the query.\\n \\n@@ -568,7 +635,7 @@ def lookup_commons(query: str, require_tokens: list[str],\\n                             \"gsrlimit\": 20,\\n                             \"gsrsearch\": f\"filetype:bitmap {query}\",\\n                             \"prop\": \"imageinfo\",\\n-                            \"iiprop\": \"url|extmetadata\",\\n+                            \"iiprop\": \"url|size|extmetadata\",\\n                             \"iiurlwidth\": 1280,\\n                         })\\n     if resp.status_code != 200:\\n@@ -587,6 +654,10 @@ def lookup_commons(query: str, require_tokens: list[str],\\n         if tokens and (not all(token_hits) if require_all\\n                        else not any(token_hits)):\\n             continue\\n+        if required_model:\\n+            from image_selection import model_matches\\n+            if not model_matches(required_model, title):\\n+                continue\\n         for info in page.get(\"imageinfo\") or []:\\n             meta = info.get(\"extmetadata\") or {}\\n             license_name = _strip_html(\\n@@ -620,11 +691,20 @@ def lookup_commons(query: str, require_tokens: list[str],\\n             count = (usage or {}).get(image_key(candidate), 0)\\n             if count >= MAX_STOCK_REUSE:\\n                 continue\\n-            if not prefer_recent and not usage:\\n+            if not prefer_recent and not prefer_landscape and not usage:\\n                 return candidate\\n-            candidates.append((-count, photo_year or 0 if prefer_recent else 0,\\n+            try:\\n+                width = int(info.get(\"width\") or 0)\\n+                height = int(info.get(\"height\") or 0)\\n+                ratio = width / height if height else 0\\n+            except (TypeError, ValueError, ZeroDivisionError):\\n+                width, ratio = 0, 0\\n+            landscape = int(1.25 <= ratio <= 2.5) if prefer_landscape else 0\\n+            resolution = min(width, 4000) if prefer_landscape else 0\\n+            candidates.append((-count, landscape, resolution,\\n+                               photo_year or 0 if prefer_recent else 0,\\n                                -int(page.get(\"index\", 99)), candidate))\\n-    return max(candidates, key=lambda row: row[:3])[3] if candidates else None\\n+    return max(candidates, key=lambda row: row[:-1])[-1] if candidates else None\\n \\n \\n def resolve_image(article: dict, *, usage=None, diagnostics=None) -> dict | None:\\n@@ -676,11 +756,33 @@ def commons(query, tokens, **kwargs):\\n         # within A350-900); an exact phrase prefilter would discard them first.\\n         photo = commons(query, [airline] if airline else [],\\n                         subject=query.removesuffix(\" aircraft\"), require_all=True,\\n+                        prefer_landscape=True,\\n                         reject_title_re=BAD_AIRCRAFT_TITLE_RE)\\n     elif airline:\\n-        photo = commons(f\"{airline} aircraft exterior\", [airline], subject=airline,\\n-                        min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\\n-                        prefer_recent=True, reject_title_re=_BAD_AIRLINE_IMAGE_RE)\\n+        profile = airline_visual_profile(airline)\\n+        role = airline_visual_role(article)\\n+        preferred = preferred_airline_models(article, airline)\\n+        for representative in preferred[:MAX_PROFILE_MODEL_QUERIES]:\\n+            photo = commons(\\n+                f\"{airline} {representative}\", [airline],\\n+                subject=f\"{airline} {representative}\", require_all=True,\\n+                required_model=representative,\\n+                min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\\n+                prefer_recent=True, prefer_landscape=True,\\n+                reject_title_re=_BAD_AIRLINE_IMAGE_RE)\\n+            if photo:\\n+                break\\n+        # Unprofiled cargo carriers still receive a cargo-specific query.  A\\n+        # profiled passenger airline with no verified cargo fleet fails closed.\\n+        if not preferred and (role != \"cargo\" or not profile):\\n+            generic_query = (f\"{airline} cargo aircraft\"\\n+                             if role == \"cargo\"\\n+                             else f\"{airline} aircraft exterior\")\\n+            photo = commons(\\n+                generic_query, [airline], subject=airline,\\n+                min_year=_article_year(article)-GENERIC_AIRLINE_MAX_PHOTO_AGE_YEARS,\\n+                prefer_recent=True, prefer_landscape=True,\\n+                reject_title_re=_BAD_AIRLINE_IMAGE_RE)\\n     else:\\n         org = find_org(article)\\n         if org:\\n@@ -57,15 +57,11 @@ def main() -> int:\\n     assert english is not None\\n     assert english[\"matched\"] == \"topic:drone\"\\n \\n-    # A China Airlines headline gets an aircraft file photo, not a cabin or\\n-    # lounge image; a body-only comparison mention must not trigger it.\\n-    china = image_fallbacks.topic_image(\\n+    # Airlines use the shared verified fleet-profile search.  Topic fallbacks\\n+    # must not give one carrier a permanently hard-coded aircraft photo.\\n+    assert image_fallbacks.topic_image(\\n         article(\"華航籌集公益款項捐贈團體\", \"中華航空公布會員哩程捐贈成果\")\\n-    )\\n-    assert china is not None\\n-    assert china[\"matched\"] == \"topic:china-airlines\"\\n-    assert china[\"subject\"] == \"China Airlines Boeing 737-800\"\\n-    assert \"Boeing_737-800\" in china[\"url\"]\\n+    ) is None\\n     assert image_fallbacks.topic_image(\\n         article(\"星宇航空公布新航線\", \"華航也在同篇背景資料中被提及\")\\n     ) is None\\n@@ -26,6 +26,18 @@ def photo(filename, **extra):\\n \\n \\n class ImageSelectionTests(unittest.TestCase):\\n+    def test_visual_profiles_are_auditable_and_not_a_single_airline_exception(self):\\n+        profiles = list(images._visual_profiles.values())\\n+        self.assertGreaterEqual(len(profiles), 9)\\n+        for profile in profiles:\\n+            with self.subTest(airline=profile.get(\"airline\")):\\n+                self.assertTrue(str(profile.get(\"source\") or \"\").startswith(\"https://\"))\\n+                self.assertRegex(str(profile.get(\"verified_utc\") or \"\"), r\"^20\\d\\d-\")\\n+                self.assertIsInstance(profile.get(\"flagship\"), list)\\n+                self.assertFalse(\\n+                    set(profile.get(\"flagship\") or [])\\n+                    & set(profile.get(\"excluded_generic\") or []))\\n+\\n     def test_medical_logistics_cannot_use_consumer_quadcopter(self):\\n         im = photo(\"Quadcopter_Drone_in_flight\", matched=\"topic:drone\")\\n         for title in (\"JEDSY medical logistics drone partnership\",\\n@@ -107,6 +119,62 @@ def test_topic_rule_does_not_override_model(self):\\n         a = article(\"China Airlines A350-1000 delivery\")\\n         self.assertIsNone(image_fallbacks.topic_image(a, allow_airport_lookup=False))\\n \\n+    def test_airline_profiles_apply_the_same_role_rules_across_carriers(self):\\n+        cases = (\\n+            (\"China Airlines reports annual earnings\", \"Airbus A350-900\"),\\n+            (\"EVA Air reports annual earnings\", \"Boeing 787-10\"),\\n+            (\"STARLUX Airlines reports annual earnings\", \"Airbus A350-1000\"),\\n+            (\"Delta Air Lines reports annual earnings\", \"Airbus A350-900\"),\\n+        )\\n+        for title, expected in cases:\\n+            a = article(title, entities={\"airlines\": [title.split(\" reports\")[0]]})\\n+            airline = images.find_airline(a)\\n+            self.assertEqual(images.preferred_airline_models(a, airline)[0], expected)\\n+\\n+    def test_airline_profiles_separate_cargo_from_passenger_images(self):\\n+        a = article(\"China Airlines cargo revenue rises\",\\n+                    entities={\"airlines\": [\"China Airlines\"]})\\n+        self.assertEqual(images.airline_visual_role(a), \"cargo\")\\n+        self.assertEqual(images.preferred_airline_models(a, \"China Airlines\")[:2],\\n+                         [\"Boeing 777F\", \"Boeing 747-400F\"])\\n+        self.assertEqual(\\n+            images.profiled_airline_stock_reason(\\n+                a, \"China Airlines\", \"China Airlines Airbus A350-900\"),\\n+            \"airline-role-mismatch\")\\n+\\n+    def test_nonrepresentative_generic_stock_is_rejected_but_exact_story_is_not(self):\\n+        generic = article(\"China Airlines reports annual earnings\",\\n+                          entities={\"airlines\": [\"China Airlines\"]})\\n+        old = photo(\"China_Airlines_Boeing_737-800\")\\n+        self.assertEqual(policy.rejection_reason(generic, old),\\n+                         \"nonrepresentative-airline-stock\")\\n+        exact = article(\"China Airlines Boeing 737-800 maintenance update\",\\n+                        entities={\"airlines\": [\"China Airlines\"],\\n+                                  \"aircraft_models\": [\"Boeing 737-800\"]})\\n+        self.assertIsNone(policy.rejection_reason(exact, old))\\n+\\n+    def test_unprofiled_airline_is_not_guessed(self):\\n+        a = article(\"Air Astana reports annual earnings\",\\n+                    entities={\"airlines\": [\"Air Astana\"]})\\n+        self.assertEqual(images.preferred_airline_models(a, \"Air Astana\"), [])\\n+\\n+    def test_published_china_airlines_737_photos_require_a_737_story(self):\\n+        root = Path(__file__).resolve().parents[1] / \"data\" / \"articles\"\\n+        violations = []\\n+        for path in root.glob(\"*.json\"):\\n+            batch = json.loads(path.read_text(encoding=\"utf-8\"))\\n+            for row in batch.get(\"articles\") or []:\\n+                image = row.get(\"image\") or {}\\n+                if not isinstance(image, dict):\\n+                    continue\\n+                identity = f\"{image.get('url', '')} {image.get('subject', '')}\"\\n+                if \"China_Airlines_Boeing_737\" not in identity and \\\\n+                        \"China Airlines Boeing 737\" not in identity:\\n+                    continue\\n+                if \"737\" not in images.article_headline_text(row):\\n+                    violations.append(row.get(\"id\"))\\n+        self.assertEqual(violations, [])\\n+\\n     def test_captioned_rss_wrong_carrier_is_rejected(self):\\n         a = article(\"Alaska Airlines sued\", entities={\"airlines\": [\"Alaska Airlines\"]}, sources=[{\"url\": \"https://publisher.test/story\"}])\\n         url = \"https://cdn.test/photo.jpg\"\\n@@ -197,6 +265,28 @@ def json(self): return payload\\n                                           article=a, require_all=True, usage={policy.image_key(first): 1})\\n         self.assertEqual(found[\"url\"], second[\"url\"])\\n \\n+    def test_landscape_aircraft_photo_beats_portrait_result(self):\\n+        a = article(\"Delta Air Lines Airbus A350\")\\n+        payload = {\"query\": {\"pages\": {\\n+            \"1\": {\"index\": 0, \"title\": \"File:Delta Air Lines Airbus A350-941 portrait.jpg\",\\n+                  \"imageinfo\": [{\"thumburl\": \"https://upload.wikimedia.org/portrait.jpg\",\\n+                                 \"width\": 1200, \"height\": 1800,\\n+                                 \"extmetadata\": {\"LicenseShortName\": {\"value\": \"CC BY 4.0\"}}}]},\\n+            \"2\": {\"index\": 1, \"title\": \"File:Delta Air Lines Airbus A350-941 landscape.jpg\",\\n+                  \"imageinfo\": [{\"thumburl\": \"https://upload.wikimedia.org/landscape.jpg\",\\n+                                 \"width\": 2400, \"height\": 1400,\\n+                                 \"extmetadata\": {\"LicenseShortName\": {\"value\": \"CC BY 4.0\"}}}]},\\n+        }}}\\n+        class Response:\\n+            status_code = 200\\n+            def json(self): return payload\\n+        with patch.object(images.requests, \"get\", return_value=Response()):\\n+            found = images.lookup_commons(\\n+                \"Delta Air Lines Airbus A350\", [\"Delta Air Lines\"],\\n+                article=a, require_all=True, required_model=\"Airbus A350-900\",\\n+                prefer_landscape=True)\\n+        self.assertEqual(found[\"url\"], \"https://upload.wikimedia.org/landscape.jpg\")\\n+\\n     def test_failed_provider_continues_to_next(self):\\n         a = article(\"Delta Air Lines Airbus A350\")\\n         im = photo(\"Delta_Air_Lines_Airbus_A350\")\\n@@ -344,10 +344,11 @@ def reset():\\n     \"a-generic-china\", \"China Airlines provides relief flight assistance\")\\n generic_china_story[\"entities\"][\"airlines\"] = [\"China Airlines\"]\\n old_generic_airline_image = {\\n-    \"url\": \"https://upload.wikimedia.org/China_Airlines_Boeing_737-800.jpg\",\\n+    \"url\": \"https://upload.wikimedia.org/China_Airlines_Airbus_A350-900.jpg\",\\n     \"link\": \"https://commons.wikimedia.org/w/old\",\\n     \"provider\": \"Wikimedia Commons\", \"kind\": \"file_photo\",\\n-    \"matched\": \"China Airlines aircraft\", \"subject\": \"China Airlines\",\\n+    \"matched\": \"China Airlines Airbus A350-900\",\\n+    \"subject\": \"China Airlines Airbus A350-900\",\\n }\\n fresh_generic_airline_image = dict(old_generic_airline_image,\\n                                    photoYear=now_utc().year)"
}
