@aglyn/plugins-ai 1.0.0-beta.284 → 1.0.0-beta.287

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/package.json +17 -17
  2. package/src/lib/assets/site-styles/nonprofit.webp +0 -0
  3. package/src/lib/assets/site-styles/professional.webp +0 -0
  4. package/src/lib/assets/site-styles/store.webp +0 -0
  5. package/src/lib/assets/site-styles/trades.webp +0 -0
  6. package/src/lib/components/ai-theme-proposal-card.component.js +12 -3
  7. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  8. package/src/lib/declarations.js +5 -2
  9. package/src/lib/declarations.js.map +1 -1
  10. package/src/lib/declarations.server.js +12 -0
  11. package/src/lib/declarations.server.js.map +1 -1
  12. package/src/lib/jobs/ai-build-capabilities.d.ts +3 -1
  13. package/src/lib/jobs/ai-build-capabilities.js +27 -2
  14. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  15. package/src/lib/jobs/ai-image-reads.d.ts +59 -0
  16. package/src/lib/jobs/ai-image-reads.js +138 -0
  17. package/src/lib/jobs/ai-image-reads.js.map +1 -0
  18. package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
  19. package/src/lib/jobs/ai-job-build-step.js +83 -12
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
  22. package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
  23. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
  25. package/src/lib/jobs/ai-job-import-step.js +15 -11
  26. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  27. package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
  28. package/src/lib/jobs/ai-job-insight-step.js +23 -5
  29. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  30. package/src/lib/jobs/ai-job-layout-language.js +2 -1
  31. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
  33. package/src/lib/jobs/ai-job-media-step.js +6 -8
  34. package/src/lib/jobs/ai-job-media-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
  36. package/src/lib/jobs/ai-job-navigation-step.js +7 -9
  37. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  38. package/src/lib/jobs/ai-job-page-language.js +7 -1
  39. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  40. package/src/lib/jobs/ai-job-page-sections.js +16 -3
  41. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-step.js +9 -1
  43. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
  45. package/src/lib/jobs/ai-job-plan-step.js +78 -13
  46. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  47. package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
  48. package/src/lib/jobs/ai-job-privacy-step.js +14 -15
  49. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
  50. package/src/lib/jobs/ai-job-products-step.js +5 -3
  51. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
  53. package/src/lib/jobs/ai-job-settings-step.js +7 -8
  54. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  55. package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
  56. package/src/lib/jobs/ai-job-site-content.js +67 -8
  57. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  58. package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
  59. package/src/lib/jobs/ai-job-site-datasets.js +298 -30
  60. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  61. package/src/lib/jobs/ai-job-site-step.js +6 -3
  62. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
  64. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
  65. package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
  66. package/src/lib/jobs/ai-job-tracking-step.js +7 -8
  67. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  68. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
  69. package/src/lib/jobs/ai-jobs-beat.js +18 -8
  70. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  71. package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
  72. package/src/lib/jobs/ai-jobs-notify.js +136 -3
  73. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  74. package/src/lib/jobs/ai-layout-stock-photos.d.ts +59 -4
  75. package/src/lib/jobs/ai-layout-stock-photos.js +897 -81
  76. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  77. package/src/lib/jobs/ai-product-image.d.ts +14 -0
  78. package/src/lib/jobs/ai-product-image.js +73 -21
  79. package/src/lib/jobs/ai-product-image.js.map +1 -1
  80. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
  81. package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
  82. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  83. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
  84. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
  85. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
  86. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
  87. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
  88. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
  89. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
  90. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
  91. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
  92. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
  93. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
  94. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
  95. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
  96. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
  97. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
  98. package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
  99. package/src/lib/layout-language/ai-layout-compiler.js +194 -18
  100. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  101. package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
  102. package/src/lib/layout-language/ai-layout-copy.js +383 -3
  103. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  104. package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
  105. package/src/lib/layout-language/ai-layout-design.js +31 -11
  106. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  107. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
  108. package/src/lib/layout-language/ai-layout-listings.js +9 -1
  109. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  110. package/src/lib/layout-language/ai-layout-pictures.d.ts +30 -0
  111. package/src/lib/layout-language/ai-layout-pictures.js +131 -3
  112. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  113. package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
  114. package/src/lib/layout-language/ai-layout-storefront.js +100 -20
  115. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
  116. package/src/lib/model/ai-build-job.d.ts +7 -0
  117. package/src/lib/model/ai-build-job.js +36 -7
  118. package/src/lib/model/ai-build-job.js.map +1 -1
  119. package/src/lib/model/ai-build-plan.d.ts +28 -4
  120. package/src/lib/model/ai-build-plan.js +49 -9
  121. package/src/lib/model/ai-build-plan.js.map +1 -1
  122. package/src/lib/model/ai-build-progress.js +3 -1
  123. package/src/lib/model/ai-build-progress.js.map +1 -1
  124. package/src/lib/model/ai-job-notice.d.ts +8 -0
  125. package/src/lib/model/ai-job-notice.js +33 -0
  126. package/src/lib/model/ai-job-notice.js.map +1 -1
  127. package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
  128. package/src/lib/model/ai-job-plan-lines.js +6 -1
  129. package/src/lib/model/ai-job-plan-lines.js.map +1 -1
  130. package/src/lib/model/ai-jobs.types.d.ts +8 -0
  131. package/src/lib/model/ai-jobs.types.js.map +1 -1
  132. package/src/lib/model/ai-permissions.d.ts +13 -0
  133. package/src/lib/model/ai-permissions.js +29 -1
  134. package/src/lib/model/ai-permissions.js.map +1 -1
  135. package/src/lib/model/ai-products.d.ts +18 -0
  136. package/src/lib/model/ai-products.js +32 -0
  137. package/src/lib/model/ai-products.js.map +1 -1
  138. package/src/lib/model/ai-site-inspiration.d.ts +3 -2
  139. package/src/lib/model/ai-site-inspiration.js +3 -2
  140. package/src/lib/model/ai-site-inspiration.js.map +1 -1
  141. package/src/lib/plugin.js +4 -13
  142. package/src/lib/plugin.js.map +1 -1
  143. package/src/lib/providers/anthropic.js +9 -2
  144. package/src/lib/providers/anthropic.js.map +1 -1
  145. package/src/lib/providers/catalog.js +14 -5
  146. package/src/lib/providers/catalog.js.map +1 -1
  147. package/src/lib/providers/contract.d.ts +24 -1
  148. package/src/lib/providers/contract.js.map +1 -1
  149. package/src/lib/providers/routing.js +5 -2
  150. package/src/lib/providers/routing.js.map +1 -1
  151. package/src/lib/runtime/ai-dataset-generation.d.ts +99 -1
  152. package/src/lib/runtime/ai-dataset-generation.js +251 -3
  153. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  154. package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
  155. package/src/lib/runtime/ai-dev-replay.js +4 -2
  156. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  157. package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
  158. package/src/lib/runtime/ai-doctrine-validators.js +343 -27
  159. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  160. package/src/lib/runtime/ai-doctrine.js +8 -2
  161. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  162. package/src/lib/runtime/ai-icon-library.d.ts +27 -1
  163. package/src/lib/runtime/ai-icon-library.js +30 -3
  164. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  165. package/src/lib/runtime/ai-products-generation.d.ts +11 -1
  166. package/src/lib/runtime/ai-products-generation.js +51 -4
  167. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  168. package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
  169. package/src/lib/runtime/assist-answer-cache.js +5 -2
  170. package/src/lib/runtime/assist-answer-cache.js.map +1 -1
  171. package/src/lib/server/ai-allotments.js +7 -2
  172. package/src/lib/server/ai-allotments.js.map +1 -1
  173. package/src/lib/server/ai-experiment-versions.js +5 -5
  174. package/src/lib/server/ai-experiment-versions.js.map +1 -1
  175. package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
  176. package/src/lib/server/ai-host-permissions-roster.js +33 -0
  177. package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
  178. package/src/lib/server/ai-host-permissions.js +8 -6
  179. package/src/lib/server/ai-host-permissions.js.map +1 -1
  180. package/src/lib/server/ai-image-reads-route.d.ts +17 -0
  181. package/src/lib/server/ai-image-reads-route.js +106 -0
  182. package/src/lib/server/ai-image-reads-route.js.map +1 -0
  183. package/src/lib/server/ai-seo-apply.js +5 -5
  184. package/src/lib/server/ai-seo-apply.js.map +1 -1
  185. package/src/lib/server/ai-site-import-route.js +7 -4
  186. package/src/lib/server/ai-site-import-route.js.map +1 -1
  187. package/src/lib/server/assist-build.js +1 -0
  188. package/src/lib/server/assist-build.js.map +1 -1
  189. package/src/lib/server/assist-chat.d.ts +11 -7
  190. package/src/lib/server/assist-chat.js +185 -48
  191. package/src/lib/server/assist-chat.js.map +1 -1
  192. package/src/lib/server/assist-docs-query.d.ts +51 -0
  193. package/src/lib/server/assist-docs-query.js +57 -0
  194. package/src/lib/server/assist-docs-query.js.map +1 -0
  195. package/src/lib/server/assist-jobs-context.d.ts +102 -0
  196. package/src/lib/server/assist-jobs-context.js +227 -0
  197. package/src/lib/server/assist-jobs-context.js.map +1 -0
  198. package/src/lib/server/assist-view-context.d.ts +8 -4
  199. package/src/lib/server/assist-view-context.js +12 -7
  200. package/src/lib/server/assist-view-context.js.map +1 -1
  201. package/src/lib/server.js +12 -0
  202. package/src/lib/server.js.map +1 -1
  203. package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
  204. package/src/lib/tools/ai-dataset-tool.js +24 -7
  205. package/src/lib/tools/ai-dataset-tool.js.map +1 -1
  206. package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
  207. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  208. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
  209. package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
  210. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
@@ -140,6 +140,20 @@ import { aiStockBusinessNoun } from "./ai-stock-business-nouns.js";
140
140
  * where this module lists it ("gym" of "Synchronicity Gym"); and a
141
141
  * training word's other senses (a railway's train, an elevator's lift, a
142
142
  * newspaper's press, a boat's row) never fill a gym's picture.
143
+ * 10. **The trade, never a word of it** (the 2026-10-25 booth kit, AGL-4065:
144
+ * six trades whose photos were race cars, cyclists, horses, a Honda
145
+ * scooter, a canoe for "Parts for the ranch", hotel buffets for a taco
146
+ * trailer and an office for a landscaper). A business named by two
147
+ * things ("Motorcycle and dirt bike repair shop") is both; the craft
148
+ * named first leads, never one named only in what a customer may ask
149
+ * ("takes catering requests"); a hero's first tag and a broad search's
150
+ * hit name the business's own world ({@link AiStockSiteTerms.lead}),
151
+ * never any word of its type ("race" of "race car"); a two-word world
152
+ * ("dirt bike") is named whole; a maker's name never shows; a sentence to
153
+ * the reader, a job or an order is never a subject, and a part or gear is
154
+ * searched only with its world; a record's cover is searched in its world
155
+ * first; and a listed world widens only where the library knew nothing of
156
+ * it, never an outdoor or a trade world to an office.
143
157
  *
144
158
  * A slot the library could not fill because it never answered — down, or
145
159
  * out of time before its first answer — or because the site's library
@@ -211,7 +225,43 @@ import { aiStockBusinessNoun } from "./ai-stock-business-nouns.js";
211
225
  });
212
226
  }
213
227
  const words = (list)=>new Set(list.trim().split(/\s+/));
214
- /** Words that carry no subject. */ const STOPWORDS = words('a an the and or of in on at to for with from by into onto over under near our your their my his her its ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company services service page photo picture image showing shows show ' + 'someone people person one two three some many several');
228
+ /** Words that carry no subject. */ const STOPWORDS = words('a an the and or of in on at to for with from by into onto over under near our your their my his her its ' + 'this that these those is are be being was were who whom which where while as about than then very just ' + 'small local best new top quality premium family-owned family owned independent professional ' + 'business company services service page photo picture image showing shows show ' + 'someone people person one two three some many several ' + // A sentence to the reader, never what a picture shows (AGL-4065): "Tell us
229
+ // the bike and we will say if it will fit" searched "tell us bike".
230
+ 'we us you they them i me if tell say describe send ask need want get let know see find will would could');
231
+ /**
232
+ * Words of a sentence to the reader that a business type may use as a name
233
+ * ("IT support", "Fit club", "call center"): never a picture's subject
234
+ * (AGL-4065), always a business's own word.
235
+ */ const SENTENCE_WORDS = words('it fit can call');
236
+ /**
237
+ * Nouns that never stand alone as what a picture shows (AGL-4065): a job, a
238
+ * request, an order. "Describe your job" searched "handyman describe job" and
239
+ * placed a job application form; "OEM ATV Parts Order" searched "atv parts
240
+ * order" and placed trees tagged "order".
241
+ */ const GENERIC_OBJECTS = words('job jobs request requests order orders service services detail details thing things stuff action cta');
242
+ /**
243
+ * Things a world has many of, which a photo shows only in that world
244
+ * (AGL-4065): a part, gear, a machine. Searched only with the site's own
245
+ * world ("dirt bike parts"), never alone: "parts" answered a canoe on a
246
+ * river, tagged "part", for a dirt bike shop's "Parts for the ranch".
247
+ */ const WORLD_OBJECTS = words('part parts gear gears machine machines equipment spare spares accessory accessories');
248
+ /**
249
+ * The verbs a business type's clause opens with when it says what a customer
250
+ * may ask of it (AGL-4065): "takes quote requests", "books repair
251
+ * appointments". The verb is never the business's world.
252
+ */ const TRANSACTION_VERBS = words('take takes taking book books booking accept accepts accepting welcome welcomes');
253
+ /**
254
+ * What a business does with a customer, never what its photos show
255
+ * (AGL-4065), as stems: dropped from its world. "Special orders" named
256
+ * Berm & Bolt's world, so a delivery man's "order" and trees tagged "order"
257
+ * filled its products.
258
+ */ const TRANSACTION_STEMS = new Set(// "Takes" and "books" are verbs only where they open a clause (TRANSACTION_VERBS): a bookshop's books are its world.
259
+ 'booking appointment order request quote estimate free inquiry pickup special around town'.split(' '));
260
+ /**
261
+ * Words of a business's own clause that name its world only beside another
262
+ * (AGL-4065): "race" and "team" of "amateur motocross race team" are a race
263
+ * car's and an office's as often. Never its own world alone.
264
+ */ const LEAD_GENERIC = words('amateur pro race racing team club crew group repair repairs service home house food truck trailer mobile ' + 'street event events custom family families');
215
265
  /**
216
266
  * Words that describe a picture's look, not what it shows, dropped from a
217
267
  * subject. A word that is as often a noun ("light", "set") is not one.
@@ -547,6 +597,96 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
547
597
  ].map(aiStockStem))
548
598
  };
549
599
  });
600
+ /**
601
+ * Makers whose names a photo's tags or its page address give, which no
602
+ * site's pictures carry unless its own words name the maker (AGL-4065): the
603
+ * Rut Runners Racing heroes were race cars in a pit lane under their makers'
604
+ * logos, and Holeshot Moto Repair's a Honda scooter and an Indian cruiser. A
605
+ * name that is also a common word ("apple", "puma", "triumph", "indian") is
606
+ * named only in the phrase that makes it the maker's. Read from the tags and
607
+ * the address, so a logo no tag names still shows: the live check is where
608
+ * that is seen.
609
+ */ const MAKERS = [
610
+ 'porsche',
611
+ 'ferrari',
612
+ 'lamborghini',
613
+ 'bmw',
614
+ 'mercedes',
615
+ 'audi',
616
+ 'volkswagen',
617
+ 'vw',
618
+ 'toyota',
619
+ 'honda',
620
+ 'yamaha',
621
+ 'kawasaki',
622
+ 'suzuki',
623
+ 'ktm',
624
+ 'ducati',
625
+ 'harley',
626
+ 'harley-davidson',
627
+ 'husqvarna',
628
+ 'gasgas',
629
+ 'tesla',
630
+ 'ford',
631
+ 'chevrolet',
632
+ 'chevy',
633
+ 'dodge',
634
+ 'jeep',
635
+ 'nissan',
636
+ 'subaru',
637
+ 'mazda',
638
+ 'indian motorcycle',
639
+ 'triumph motorcycle',
640
+ 'gran turismo',
641
+ 'playstation',
642
+ 'xbox',
643
+ 'nintendo',
644
+ 'nike',
645
+ 'adidas',
646
+ 'reebok',
647
+ 'red bull',
648
+ 'monster energy',
649
+ 'coca cola',
650
+ 'cocacola',
651
+ 'pepsi',
652
+ 'iphone',
653
+ 'ipad',
654
+ 'macbook',
655
+ 'imac',
656
+ 'samsung',
657
+ 'starbucks',
658
+ 'mcdonalds',
659
+ 'mcdonald'
660
+ ];
661
+ /** The makers as stems and stem phrases, as a hit's words are read. */ const MAKER_STEMS = new Set(MAKERS.map((maker)=>stemList(maker).join(' ')));
662
+ /**
663
+ * A phrase's words and each pair of them in turn, as stems (AGL-4065): "dirt
664
+ * bike" names the motocross world where "bike" alone names a bicycle's too.
665
+ */ function withPairs(list) {
666
+ const out = [
667
+ ...list
668
+ ];
669
+ for(let index = 0; index + 1 < list.length; index += 1)out.push(`${list[index]} ${list[index + 1]}`);
670
+ return out;
671
+ }
672
+ /** A text's stems and stem pairs, every tag or phrase read on its own. */ function termsOf(texts) {
673
+ return new Set(texts.flatMap((text)=>withPairs(stemList(text))));
674
+ }
675
+ /** A word or phrase as stems: "dirt bikes" is "dirt bike". */ const stemPhrase = (phrase)=>stemList(phrase).join(' ');
676
+ /**
677
+ * A tag that says what a photo suits, never what it shows (AGL-4065):
678
+ * "samsung wallpaper", "iphone wallpaper", "lock screen". A latte tagged so
679
+ * shows no phone.
680
+ */ const SUITS = /\b(?:wallpapers?|backgrounds?|screens?|screensavers?|lockscreen|4k|hd)\b/i;
681
+ /** The words a photo names a maker by: its tags, those of what it suits left out, its description and its page address. */ function aiStockMakerTerms(photo) {
682
+ var _photo_tags, _photo_alt;
683
+ const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).filter((tag)=>!SUITS.test(tag));
684
+ return termsOf([
685
+ ...tags,
686
+ (_photo_alt = photo.alt) != null ? _photo_alt : '',
687
+ pageSlug(photo.pageUrl)
688
+ ]);
689
+ }
550
690
  /**
551
691
  * Hands are never a picture's subject (AGL-3660): "hand-poured", "handmade"
552
692
  * and "by hand" say how a thing was made, and a search for "hands" found
@@ -735,11 +875,73 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
735
875
  'cream jar',
736
876
  'moisturizer'
737
877
  ]
878
+ },
879
+ {
880
+ // A rider's gear is what they wear (AGL-4065): "Riding Gear Item" took a
881
+ // gear wheel of a transmission, tagged "gear, mechanics, bikes".
882
+ objects: [
883
+ 'gear'
884
+ ],
885
+ within: [
886
+ 'motocross',
887
+ 'motorcycle',
888
+ 'motorbike',
889
+ 'dirtbike',
890
+ 'dirt bike',
891
+ 'enduro',
892
+ 'atv'
893
+ ],
894
+ kin: [
895
+ 'riding gear',
896
+ 'motocross gear',
897
+ 'protective gear',
898
+ 'helmet',
899
+ 'helmets',
900
+ 'motocross helmet',
901
+ 'motorcycle helmet',
902
+ 'jersey',
903
+ 'boots',
904
+ 'goggles',
905
+ 'gloves'
906
+ ],
907
+ queries: [
908
+ 'motocross gear',
909
+ 'motocross helmet'
910
+ ]
911
+ },
912
+ {
913
+ // A dirt bike's tire is knobby; a car's on a highway is not one (AGL-4065).
914
+ objects: [
915
+ 'tire'
916
+ ],
917
+ within: [
918
+ 'motocross',
919
+ 'motorcycle',
920
+ 'motorbike',
921
+ 'dirtbike',
922
+ 'dirt bike',
923
+ 'enduro',
924
+ 'atv'
925
+ ],
926
+ kin: [
927
+ 'tire',
928
+ 'tires',
929
+ 'knobby tire',
930
+ 'motocross tire',
931
+ 'dirt bike tire',
932
+ 'motorcycle tire',
933
+ 'bike tire',
934
+ 'wheel'
935
+ ],
936
+ queries: [
937
+ 'dirt bike tire',
938
+ 'motocross tire'
939
+ ]
738
940
  }
739
941
  ];
740
942
  /** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
741
943
  var _OBJECT_KIN_find;
742
- return (_OBJECT_KIN_find = OBJECT_KIN.find((entry)=>entry.objects.some((word)=>aiStockStem(word) === object) && entry.within.some((word)=>world.includes(aiStockStem(word))))) != null ? _OBJECT_KIN_find : null;
944
+ return (_OBJECT_KIN_find = OBJECT_KIN.find((entry)=>entry.objects.some((word)=>aiStockStem(word) === object) && entry.within.some((word)=>world.includes(stemPhrase(word))))) != null ? _OBJECT_KIN_find : null;
743
945
  }
744
946
  /**
745
947
  * The kin of a few common crafts: the words a photo of that world is
@@ -1389,6 +1591,362 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1389
1591
  'analog'
1390
1592
  ]
1391
1593
  }
1594
+ },
1595
+ // The six trades of the 2026-10-25 booth kit (AGL-4065), none of which the
1596
+ // table listed: a landscaper and a handyman widened to an office, three
1597
+ // motocross businesses took race cars, cyclists, horses and scooters, and a
1598
+ // taco trailer that also takes catering requests was read as a caterer.
1599
+ {
1600
+ // A landscaper's photos are lawns, beds and gardens at work; a zen garden
1601
+ // of raked sand is not one (Bluebonnet's irrigation section).
1602
+ match: [
1603
+ 'landscaping',
1604
+ 'landscaper',
1605
+ 'landscapers',
1606
+ 'lawn',
1607
+ 'lawns',
1608
+ 'xeriscape',
1609
+ 'xeriscaping',
1610
+ 'sod',
1611
+ 'irrigation',
1612
+ 'hardscape',
1613
+ 'hardscaping',
1614
+ 'gardening',
1615
+ 'gardener',
1616
+ 'gardeners'
1617
+ ],
1618
+ kin: [
1619
+ 'landscaping',
1620
+ 'landscape',
1621
+ 'landscaper',
1622
+ 'lawn',
1623
+ 'grass',
1624
+ 'garden',
1625
+ 'yard',
1626
+ 'mower',
1627
+ 'mowing',
1628
+ 'lawn mower',
1629
+ 'turf',
1630
+ 'hedge',
1631
+ 'shrub',
1632
+ 'mulch',
1633
+ 'sprinkler',
1634
+ 'planting',
1635
+ 'flowerbed',
1636
+ 'flower bed',
1637
+ 'backyard',
1638
+ 'gardening',
1639
+ 'gardener',
1640
+ 'xeriscape',
1641
+ 'sod',
1642
+ 'irrigation'
1643
+ ],
1644
+ broad: [
1645
+ 'lawn care',
1646
+ 'landscaping garden',
1647
+ 'garden landscaping'
1648
+ ],
1649
+ making: [
1650
+ 'landscaper mowing lawn',
1651
+ 'gardener planting'
1652
+ ],
1653
+ shunLead: [
1654
+ 'zen',
1655
+ 'sand',
1656
+ 'office',
1657
+ 'business',
1658
+ 'desk',
1659
+ 'laptop',
1660
+ 'meeting',
1661
+ 'golf',
1662
+ 'stadium'
1663
+ ]
1664
+ },
1665
+ {
1666
+ // A handyman's photos are tools and repairs; never an architect's house
1667
+ // with a family before it, nor a job application (Two Oaks).
1668
+ match: [
1669
+ 'handyman',
1670
+ 'handymen',
1671
+ 'repairman',
1672
+ 'drywall',
1673
+ 'remodel',
1674
+ 'remodeling',
1675
+ 'remodeler',
1676
+ 'renovation',
1677
+ 'renovations',
1678
+ 'home repair'
1679
+ ],
1680
+ kin: [
1681
+ 'handyman',
1682
+ 'tools',
1683
+ 'tool',
1684
+ 'tool belt',
1685
+ 'toolbox',
1686
+ 'drill',
1687
+ 'hammer',
1688
+ 'screwdriver',
1689
+ 'wrench',
1690
+ 'ladder',
1691
+ 'workshop',
1692
+ 'repair',
1693
+ 'home repair',
1694
+ 'carpenter',
1695
+ 'carpentry',
1696
+ 'renovation',
1697
+ 'remodel',
1698
+ 'deck',
1699
+ 'fence',
1700
+ 'door',
1701
+ 'wall',
1702
+ 'plaster',
1703
+ 'drywall',
1704
+ 'construction'
1705
+ ],
1706
+ broad: [
1707
+ 'handyman tools',
1708
+ 'home repair',
1709
+ 'carpenter workshop'
1710
+ ],
1711
+ making: [
1712
+ 'handyman at work',
1713
+ 'handyman drilling'
1714
+ ],
1715
+ shunLead: [
1716
+ 'architecture',
1717
+ 'family',
1718
+ 'office',
1719
+ 'application',
1720
+ 'job',
1721
+ 'business',
1722
+ 'meeting',
1723
+ 'desk',
1724
+ 'laptop'
1725
+ ]
1726
+ },
1727
+ {
1728
+ // Motocross and the motorcycle world: a dirt bike, never a bicycle, a
1729
+ // race car, a horse or a scooter (Rut Runners, Holeshot, Berm & Bolt).
1730
+ match: [
1731
+ 'motocross',
1732
+ 'motorcycle',
1733
+ 'motorcycles',
1734
+ 'motorbike',
1735
+ 'motorbikes',
1736
+ 'dirtbike',
1737
+ 'dirtbikes',
1738
+ 'dirt bike',
1739
+ 'enduro',
1740
+ 'supercross',
1741
+ 'moto',
1742
+ 'mx',
1743
+ 'atv',
1744
+ 'quad bike',
1745
+ 'powersports'
1746
+ ],
1747
+ kin: [
1748
+ 'motocross',
1749
+ 'dirt bike',
1750
+ 'dirtbike',
1751
+ 'enduro',
1752
+ 'supercross',
1753
+ 'motorbike',
1754
+ 'motorcycle',
1755
+ 'mx',
1756
+ 'atv',
1757
+ 'quad bike',
1758
+ 'motocross track',
1759
+ 'motocross rider',
1760
+ 'dirt track'
1761
+ ],
1762
+ near: [
1763
+ 'rider',
1764
+ 'riders',
1765
+ 'helmet',
1766
+ 'jump',
1767
+ 'track',
1768
+ 'mud',
1769
+ 'dust',
1770
+ 'berm',
1771
+ 'racing',
1772
+ 'race',
1773
+ 'racer'
1774
+ ],
1775
+ broad: [
1776
+ 'motocross',
1777
+ 'dirt bike',
1778
+ 'motocross race',
1779
+ 'motocross rider'
1780
+ ],
1781
+ making: [
1782
+ 'motorcycle mechanic',
1783
+ 'motorcycle workshop'
1784
+ ],
1785
+ rivals: {
1786
+ words: [
1787
+ 'bicycle',
1788
+ 'bicycles',
1789
+ 'cycling',
1790
+ 'cyclist',
1791
+ 'cyclists',
1792
+ 'cycle racing',
1793
+ 'bike race',
1794
+ 'road bike',
1795
+ 'mountain bike',
1796
+ 'mtb',
1797
+ 'downhill',
1798
+ 'car',
1799
+ 'cars',
1800
+ 'race car',
1801
+ 'formula',
1802
+ 'kart',
1803
+ 'karting',
1804
+ 'horse',
1805
+ 'horses',
1806
+ 'equestrian',
1807
+ 'scooter',
1808
+ 'moped',
1809
+ 'vespa',
1810
+ 'cruiser',
1811
+ 'chopper'
1812
+ ],
1813
+ core: [
1814
+ 'motocross',
1815
+ 'motorcycle',
1816
+ 'motorbike',
1817
+ 'dirt bike',
1818
+ 'dirtbike',
1819
+ 'supercross',
1820
+ 'atv',
1821
+ 'quad bike'
1822
+ ]
1823
+ },
1824
+ shunLead: [
1825
+ 'car',
1826
+ 'scooter',
1827
+ 'moped',
1828
+ 'cruiser',
1829
+ 'chopper',
1830
+ 'vespa',
1831
+ 'bicycle',
1832
+ 'cyclist',
1833
+ 'horse',
1834
+ 'highway',
1835
+ 'asphalt',
1836
+ 'traffic'
1837
+ ]
1838
+ },
1839
+ {
1840
+ // A parts shop of the motocross world: its parts, tires and riding gear,
1841
+ // never a canoe, a delivery or an industrial lathe (Berm & Bolt).
1842
+ match: [
1843
+ 'parts',
1844
+ 'part',
1845
+ 'aftermarket',
1846
+ 'oem',
1847
+ 'spare parts'
1848
+ ],
1849
+ of: [
1850
+ 'motocross',
1851
+ 'motorcycle',
1852
+ 'motorbike',
1853
+ 'dirtbike',
1854
+ 'dirt bike',
1855
+ 'enduro',
1856
+ 'supercross',
1857
+ 'moto',
1858
+ 'atv',
1859
+ 'quad bike',
1860
+ 'powersports'
1861
+ ],
1862
+ kin: [
1863
+ 'parts',
1864
+ 'part',
1865
+ 'spare part',
1866
+ 'motorcycle parts',
1867
+ 'tire',
1868
+ 'tires',
1869
+ 'wheel',
1870
+ 'chain',
1871
+ 'sprocket',
1872
+ 'brake',
1873
+ 'exhaust',
1874
+ 'riding gear',
1875
+ 'helmet'
1876
+ ],
1877
+ broad: [
1878
+ 'dirt bike parts',
1879
+ 'motorcycle parts',
1880
+ 'motocross gear'
1881
+ ],
1882
+ making: [
1883
+ 'motorcycle mechanic',
1884
+ 'motorcycle workshop'
1885
+ ],
1886
+ shunLead: [
1887
+ 'canoe',
1888
+ 'river',
1889
+ 'delivery',
1890
+ 'food',
1891
+ 'trees',
1892
+ 'office',
1893
+ 'industry',
1894
+ 'industrial',
1895
+ 'factory'
1896
+ ]
1897
+ },
1898
+ {
1899
+ // A taco trailer's photos are tacos and the truck, never a hotel's buffet,
1900
+ // a chef in whites or raw potatoes (Pit Row Tacos).
1901
+ match: [
1902
+ 'taco',
1903
+ 'tacos',
1904
+ 'taqueria',
1905
+ 'taco truck',
1906
+ 'taco trailer',
1907
+ 'burrito',
1908
+ 'burritos'
1909
+ ],
1910
+ kin: [
1911
+ 'taco',
1912
+ 'tacos',
1913
+ 'tortilla',
1914
+ 'tortillas',
1915
+ 'salsa',
1916
+ 'mexican',
1917
+ 'mexican food',
1918
+ 'street food',
1919
+ 'food truck',
1920
+ 'taco truck',
1921
+ 'griddle',
1922
+ 'burrito',
1923
+ 'quesadilla',
1924
+ 'nachos',
1925
+ 'guacamole',
1926
+ 'carnitas',
1927
+ 'pastor',
1928
+ 'asada'
1929
+ ],
1930
+ broad: [
1931
+ 'tacos',
1932
+ 'street tacos',
1933
+ 'taco truck'
1934
+ ],
1935
+ making: [
1936
+ 'taco truck cooking',
1937
+ 'tacos on the griddle'
1938
+ ],
1939
+ shunLead: [
1940
+ 'buffet',
1941
+ 'hotel',
1942
+ 'fine dining',
1943
+ 'chef',
1944
+ 'indian',
1945
+ 'potato',
1946
+ 'banquet',
1947
+ 'cafeteria',
1948
+ 'wedding'
1949
+ ]
1392
1950
  }
1393
1951
  ];
1394
1952
  /** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
@@ -1409,28 +1967,60 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1409
1967
  return text.toLowerCase().split(/[^a-zÀ-ɏ]+/).filter((word)=>word.length > 1).map(aiStockStem);
1410
1968
  }
1411
1969
  /** Where a noun phrase ends: punctuation, a preposition, a conjunction. */ const CLAUSE_BREAK = /[,;:.()]|\s[-–—]\s|\b(?:with|on|in|at|against|beside|next|near|under|over|by|from|for|during|while|atop|inside|across|beneath|through|into|onto|amid|among|and|of|that|which|who)\b|&/;
1412
- /** A business type's clauses, each with the word that opened it. */ function businessClauses(businessType) {
1970
+ /** A clause's words that name the business: no site kind, no filler. */ const businessOf = (clause)=>clause.filter((word)=>!SITE_KINDS.has(word) && !FILLER.has(word));
1971
+ /**
1972
+ * A business type's clauses, each with the word that opened it. Where the
1973
+ * business's own clause is a word or two joined by "and" to a noun phrase
1974
+ * that ends in a place of business ("Motorcycle and dirt bike repair shop",
1975
+ * "Dirt bike and ATV parts shop"), the two are one clause (AGL-4065): the
1976
+ * Holeshot start read its business as "motorcycle" and lost the dirt bikes,
1977
+ * Berm & Bolt's as "dirt bike" and its craft as "bike". "And" before a verb
1978
+ * or a place still ends the clause. A clause that opens with what a customer
1979
+ * may ask ("takes", "books") loses that verb.
1980
+ */ function businessClauses(businessType) {
1413
1981
  const text = businessType.toLowerCase().replace(DOING, ' | ');
1414
1982
  const clauses = [];
1415
1983
  const breaker = new RegExp(`${BUSINESS_BREAK.source}|\\|`, 'g');
1416
1984
  let at = 0;
1417
1985
  let opener = '';
1986
+ const push = (raw)=>{
1987
+ var _found_;
1988
+ const found = contentWords(raw);
1989
+ // Never the type's first words: "Books and coffee" sells books.
1990
+ const asked = opener !== '' && found.length > 1 && TRANSACTION_VERBS.has((_found_ = found[0]) != null ? _found_ : '');
1991
+ clauses.push(_extends({
1992
+ opener,
1993
+ words: asked ? found.slice(1) : found
1994
+ }, asked ? {
1995
+ asked: true
1996
+ } : {}));
1997
+ };
1418
1998
  for (const match of text.matchAll(breaker)){
1419
1999
  var _match_index;
1420
- clauses.push({
1421
- opener,
1422
- words: contentWords(text.slice(at, match.index))
1423
- });
2000
+ push(text.slice(at, match.index));
1424
2001
  opener = match[0].trim();
1425
2002
  at = ((_match_index = match.index) != null ? _match_index : 0) + match[0].length;
1426
2003
  }
1427
- clauses.push({
1428
- opener,
1429
- words: contentWords(text.slice(at))
1430
- });
2004
+ push(text.slice(at));
2005
+ const own = clauses.findIndex((clause)=>businessOf(clause.words).length > 0);
2006
+ const first = clauses[own];
2007
+ const next = clauses[own + 1];
2008
+ if (first && next && (next.opener === 'and' || next.opener === '&')) {
2009
+ var _right_;
2010
+ const left = businessOf(first.words);
2011
+ const right = businessOf(next.words);
2012
+ const head = (_right_ = right[right.length - 1]) != null ? _right_ : '';
2013
+ if (left.length <= 2 && left.every((word)=>!VENUES.has(word) && !ROLES.has(word)) && right.length >= 2 && VENUES.has(head)) clauses.splice(own, 2, {
2014
+ opener: first.opener,
2015
+ words: [
2016
+ ...first.words,
2017
+ ...next.words
2018
+ ],
2019
+ joined: next.words
2020
+ });
2021
+ }
1431
2022
  return clauses;
1432
2023
  }
1433
- /** A clause's words that name the business: no site kind, no filler. */ const businessOf = (clause)=>clause.filter((word)=>!SITE_KINDS.has(word) && !FILLER.has(word));
1434
2024
  /**
1435
2025
  * The kind of business in a few words: the first clause of the business
1436
2026
  * type that says more than what kind of site it is, before a place, an
@@ -1438,9 +2028,13 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1438
2028
  * of an English noun phrase is at its end). "ceramic artist" from "a
1439
2029
  * portfolio for a ceramic artist who makes stoneware bowls"; "candle shop"
1440
2030
  * from "a small-batch candle shop selling hand-poured soy candles online".
2031
+ * A business named by two things joined by "and" is its second noun phrase
2032
+ * whole: "dirt bike repair shop" of "Motorcycle and dirt bike repair shop"
2033
+ * (AGL-4065).
1441
2034
  */ export function aiStockBusinessWords(businessType) {
1442
- for (const { words: clause } of businessClauses(businessType)){
2035
+ for (const { words: clause, joined } of businessClauses(businessType)){
1443
2036
  const named = businessOf(clause);
2037
+ if (joined) return businessOf(joined).slice(-4).join(' ');
1444
2038
  if (named.length) return named.slice(-3).join(' ');
1445
2039
  }
1446
2040
  return '';
@@ -1455,16 +2049,36 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1455
2049
  const craft = named[named.length - 1];
1456
2050
  return craft ? aiStockStem(craft) : '';
1457
2051
  }
1458
- /** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
2052
+ /** A word or phrase of a group's list, as stems: what a business type and a hit are read for. */ const groupTerm = (word)=>stemPhrase(word);
2053
+ /**
2054
+ * The site's terms, read from its business type.
2055
+ *
2056
+ * Two worlds are read (AGL-4065). Its DOMAIN is everything its words name
2057
+ * of its world, what a picture of a thing is held to. Its LEAD is the
2058
+ * business's own world: the listed crafts its own clause names (and the
2059
+ * craft's), their names and kin, and the clause's own words where no craft
2060
+ * covers them, never a word that names a world only beside another ("race",
2061
+ * "team") nor what a customer is asked to do. A hero's first tag and a broad
2062
+ * search's hit name the lead: the Rut Runners heroes, held to every word of
2063
+ * "Amateur motocross race team: rider profiles, race schedule…", took race
2064
+ * cars because "race car" named "race". Of the crafts its words list, the one
2065
+ * named first leads (a taco trailer that "takes catering requests" is a taco
2066
+ * trailer), and a craft named only in what a customer may ask for gives the
2067
+ * site no searches of its own.
2068
+ */ export function aiStockSiteTerms(businessType) {
1459
2069
  let business = aiStockBusinessWords(businessType);
1460
2070
  let craft = aiStockCraftWords(business);
1461
2071
  const domain = new Set();
1462
2072
  /** The things it makes, by their head nouns. */ const made = new Set();
2073
+ /** Stems and stem pairs of every clause that names its world, and of those that only say what it is asked for. */ const said = new Set();
2074
+ const asked = new Set();
2075
+ /** The business's own clause, as stems and stem pairs: what its lead is read from. */ let own = [];
2076
+ let ownSeen = false;
1463
2077
  /** The last word of the first clause that names the world: "soap" of "for handmade cold-process soap". */ let firstWorld = '';
1464
2078
  let named = false;
1465
2079
  let making = false;
1466
2080
  let place = false;
1467
- for (const { opener, words: clause } of businessClauses(businessType)){
2081
+ for (const { opener, words: clause, asked: isAsked } of businessClauses(businessType)){
1468
2082
  if (opener === '|') making = true;
1469
2083
  // Where it is, and who it is for, are not its world — once something has named it.
1470
2084
  if (named && PLACE_OR_AUDIENCE.test(opener)) {
@@ -1478,6 +2092,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1478
2092
  if (place && opener === ',') continue;
1479
2093
  place = false;
1480
2094
  const kept = businessOf(clause).filter((word)=>!VENUES.has(word) && !ROLES.has(word));
2095
+ if (!ownSeen && businessOf(clause).length) {
2096
+ ownSeen = true;
2097
+ own = kept.filter((word)=>!LEAD_GENERIC.has(word));
2098
+ }
1481
2099
  if (kept.length) named = true;
1482
2100
  // What it makes names its world by what the things are made of, never
1483
2101
  // the things: "stoneware" of "stoneware bowls", "soy" of "soy candles".
@@ -1485,32 +2103,55 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1485
2103
  // A set or a gift is never a world: "gift" of "gift sets" is not one.
1486
2104
  const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
1487
2105
  for (const word of world)for (const stem of stemList(word))domain.add(stem);
2106
+ for (const term of termsOf([
2107
+ kept.join(' ')
2108
+ ]))(isAsked ? asked : said).add(term);
1488
2109
  const last = world[world.length - 1];
1489
2110
  if (!firstWorld && last) firstWorld = aiStockStem(last);
1490
2111
  if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
1491
2112
  }
1492
2113
  // A business named only by what it sells ("a shop selling pottery") is of that world.
1493
2114
  if (!domain.size) for (const stem of made)domain.add(stem);
1494
- const groups = CRAFT_KIN.filter((group)=>group.match.some((word)=>domain.has(aiStockStem(word)) || made.has(aiStockStem(word)) || aiStockStem(word) === craft));
2115
+ const typeStems = stemList(businessType);
2116
+ const typeTerms = withPairs(typeStems);
2117
+ /** Where a word or a two-word name first stands in the business type; the end where it stands nowhere. */ const positionOf = (term)=>{
2118
+ const [head, tail] = term.split(' ');
2119
+ const index = typeStems.findIndex((stem, at)=>stem === head && (tail === undefined || typeStems[at + 1] === tail));
2120
+ return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
2121
+ };
2122
+ /** Where a group's list first meets the business type, its words in order. */ const at = (list)=>Math.min(...list.map((word)=>positionOf(groupTerm(word))));
2123
+ const names = (word)=>{
2124
+ const term = groupTerm(word);
2125
+ return term.includes(' ') ? said.has(term) || asked.has(term) : domain.has(term) || made.has(term) || term === craft;
2126
+ };
2127
+ const found = CRAFT_KIN.map((group, index)=>({
2128
+ group,
2129
+ index
2130
+ })).filter(({ group })=>group.match.some(names) && (!group.of || group.of.some(names))).sort((a, b)=>Number(Boolean(b.group.of)) - Number(Boolean(a.group.of)) || at(a.group.match) - at(b.group.match) || a.index - b.index);
2131
+ const groups = found.map(({ group })=>group);
2132
+ // A craft named only in what the business is asked for ("takes catering
2133
+ // requests") lends its kin and its refusals, never its searches (AGL-4065).
2134
+ const major = groups.filter((group)=>group.match.some((word)=>said.has(groupTerm(word)) || made.has(groupTerm(word))));
1495
2135
  // A business that is only a place ("an online shop for handmade
1496
2136
  // cold-process soap") takes its craft from what it is for: the Saltmarsh
1497
2137
  // Soap Co start (2026-10-10) read its craft as nothing, so its hero searched
1498
2138
  // "shop" (a florist's bouquet) and no product was held to soap.
1499
2139
  if (!craft) {
1500
- var _groups_;
1501
- const listed = (_groups_ = groups[0]) == null ? void 0 : _groups_.match.map(aiStockStem).find((stem)=>domain.has(stem) || made.has(stem));
2140
+ var _major_;
2141
+ const listed = (_major_ = major[0]) == null ? void 0 : _major_.match.map(groupTerm).find((stem)=>!stem.includes(' ') && (domain.has(stem) || made.has(stem)));
1502
2142
  craft = listed || firstWorld || [
1503
2143
  ...made
1504
2144
  ][0] || '';
1505
2145
  if (craft && !stemList(business).includes(craft)) business = `${craft} ${business}`.trim();
1506
- } else if (groups[0] && !groups.some((group)=>group.match.some((word)=>aiStockStem(word) === craft))) {
1507
- var _groups__match_map_find;
2146
+ } else if (major[0] && !major.some((group)=>group.match.some((word)=>groupTerm(word) === craft))) {
2147
+ var _major__match_map_find;
1508
2148
  // A word the table does not list, beside a craft it does, says what kind
1509
2149
  // of that craft it is (AGL-3856): "wedding" of "a wedding and portrait
1510
2150
  // photographer" is a photographer's subject, never a craft to search by.
1511
- craft = (_groups__match_map_find = groups[0].match.map(aiStockStem).find((stem)=>domain.has(stem) || made.has(stem))) != null ? _groups__match_map_find : craft;
2151
+ craft = (_major__match_map_find = major[0].match.map(groupTerm).find((stem)=>!stem.includes(' ') && (domain.has(stem) || made.has(stem)))) != null ? _major__match_map_find : craft;
1512
2152
  }
1513
2153
  if (craft) domain.add(craft);
2154
+ const craftGroup = major.find((group)=>group.match.some((word)=>groupTerm(word) === craft));
1514
2155
  const broad = [];
1515
2156
  const makingSearches = [];
1516
2157
  const rivals = [];
@@ -1520,25 +2161,72 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1520
2161
  const shun = new Set();
1521
2162
  const shunLead = new Set();
1522
2163
  for (const group of groups){
1523
- var _group_shun, _group_shunLead;
1524
- for (const word of (_group_shun = group.shun) != null ? _group_shun : [])shun.add(aiStockStem(word));
1525
- for (const word of (_group_shunLead = group.shunLead) != null ? _group_shunLead : [])shunLead.add(aiStockStem(word));
1526
- for (const word of group.match)core.add(aiStockStem(word));
1527
- for (const word of group.kin)domain.add(aiStockStem(word));
1528
- broad.push(...group.broad);
1529
- makingSearches.push(...group.making);
2164
+ var _group_shun, _group_shunLead, _group_near;
2165
+ for (const word of (_group_shun = group.shun) != null ? _group_shun : [])shun.add(groupTerm(word));
2166
+ for (const word of (_group_shunLead = group.shunLead) != null ? _group_shunLead : [])shunLead.add(groupTerm(word));
2167
+ for (const word of [
2168
+ ...group.kin,
2169
+ ...(_group_near = group.near) != null ? _group_near : []
2170
+ ])domain.add(groupTerm(word));
1530
2171
  if (group.rivals) rivals.push({
1531
- words: group.rivals.words.map(aiStockStem),
1532
- core: group.rivals.core.map(aiStockStem)
2172
+ words: group.rivals.words.map(groupTerm),
2173
+ core: group.rivals.core.map(groupTerm)
1533
2174
  });
1534
2175
  }
2176
+ for (const group of major){
2177
+ for (const word of group.match)core.add(groupTerm(word));
2178
+ broad.push(...group.broad);
2179
+ makingSearches.push(...group.making);
2180
+ }
2181
+ // A two-word name of a listed world names it whole: "dirt bike", never
2182
+ // "bike" (a bicycle's) where the type says it nowhere else (AGL-4065).
2183
+ for (const group of groups)for (const word of group.match){
2184
+ const term = groupTerm(word);
2185
+ if (!term.includes(' ') || !typeTerms.includes(term)) continue;
2186
+ domain.add(term);
2187
+ const [head, tail] = term.split(' ');
2188
+ const alone = (stem)=>typeStems.some((found, index)=>found === stem && !(found === head ? typeStems[index + 1] === tail : typeStems[index - 1] === head));
2189
+ for (const stem of [
2190
+ head,
2191
+ tail
2192
+ ])if (!alone(stem)) domain.delete(stem);
2193
+ }
2194
+ // What a customer is asked to do is never a world (AGL-4065).
2195
+ for (const stem of TRANSACTION_STEMS)domain.delete(stem);
2196
+ // The business's own world: the crafts its own clause names, and the craft's.
2197
+ const ownTerms = new Set(termsOf([
2198
+ own.join(' ')
2199
+ ]));
2200
+ const leading = major.filter((group)=>group === craftGroup || group.match.some((word)=>ownTerms.has(groupTerm(word))));
2201
+ const lead = new Set();
2202
+ for (const group of leading)for (const word of [
2203
+ ...group.match,
2204
+ ...group.kin
2205
+ ])lead.add(groupTerm(word));
2206
+ if (leading.length) {
2207
+ for (const word of own)for (const stem of stemList(word))if (![
2208
+ ...lead
2209
+ ].some((term)=>term === stem || term.includes(' ') && term.split(' ').includes(stem) && ownTerms.has(term))) lead.add(stem);
2210
+ }
2211
+ // A trade's world, named by the world it serves: "dirt bike" for a dirt bike parts shop.
2212
+ const served = (craftGroup == null ? void 0 : craftGroup.of) ? groups.filter((group)=>group !== craftGroup).flatMap((group)=>group.match).filter((word)=>typeTerms.includes(groupTerm(word))).sort((a, b)=>positionOf(groupTerm(a)) - positionOf(groupTerm(b)))[0] : undefined;
1535
2213
  if (!broad.length) broad.push(business, craft);
2214
+ // A part or gear names no world on its own: a dirt bike shop's part, never a canoe's (AGL-4065).
2215
+ const ofAWorld = (term)=>term.length > 1 && !term.split(' ').every((word)=>WORLD_OBJECTS.has(word));
1536
2216
  return _extends({
1537
2217
  business,
1538
2218
  craft,
1539
2219
  domain: [
1540
2220
  ...domain
1541
2221
  ].filter((stem)=>stem.length > 1),
2222
+ lead: leading.length ? [
2223
+ ...lead
2224
+ ].filter(ofAWorld) : [
2225
+ ...domain
2226
+ ].filter((stem)=>stem.length > 1)
2227
+ }, served ? {
2228
+ leadWord: served
2229
+ } : {}, {
1542
2230
  broad: [
1543
2231
  ...new Set(broad.map((query)=>query.trim()).filter(Boolean))
1544
2232
  ],
@@ -1559,12 +2247,12 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1559
2247
  ]
1560
2248
  } : {}, {
1561
2249
  named: [
1562
- ...new Set(stemList(businessType))
2250
+ ...new Set(typeTerms)
1563
2251
  ],
1564
2252
  rivals,
1565
2253
  core: [
1566
2254
  ...core
1567
- ],
2255
+ ].filter((term)=>!(craftGroup == null ? void 0 : craftGroup.of) || ofAWorld(term)),
1568
2256
  shun: [
1569
2257
  ...shun
1570
2258
  ],
@@ -1578,20 +2266,45 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1578
2266
  * the beta.266 Mexicano portfolio's gallery was "Project one" to "Project
1579
2267
  * four", and searched "project" (students at a whiteboard), "project four"
1580
2268
  * (lavender) and "four" (a running track's lane numbers).
1581
- */ const PLACEHOLDER_SUBJECTS = words('project projects piece pieces item items example examples sample samples entry entries untitled work works ' + 'selected recent latest ' + 'four five six seven eight nine ten first second third fourth fifth sixth seventh eighth ninth tenth');
2269
+ */ const PLACEHOLDER_SUBJECTS = words('project projects piece pieces item items example examples sample samples entry entries untitled work works ' + 'selected recent latest ' + 'four five six seven eight nine ten first second third fourth fifth sixth seventh eighth ninth tenth ' + // When a thing is, never the thing (AGL-4065): "Upcoming race 1 (details to be added)" searched "upcoming race".
2270
+ 'upcoming past next previous future');
1582
2271
  /** A phrase's content words without its filler; all filler, and nothing is left. */ function subjectOf(list) {
1583
- return list.filter((word)=>!FILLER.has(word) && !PLACEHOLDER_SUBJECTS.has(word));
2272
+ return list.filter((word)=>!FILLER.has(word) && !PLACEHOLDER_SUBJECTS.has(word) && !GENERIC_OBJECTS.has(word) && !SENTENCE_WORDS.has(word));
1584
2273
  }
1585
2274
  /**
1586
2275
  * A picture's subject in a few words (AGL-3660): the noun phrase its alt
1587
2276
  * text opens with — its first clause holding more than filler, cut where a
1588
2277
  * preposition starts the setting, filler dropped, its last three words (the
1589
- * head noun is the last) — else its section's name.
2278
+ * head noun is the last) — else its section's name. A word or two joined by
2279
+ * "and" to a longer noun phrase only modify its head: the subject of "Potato
2280
+ * and egg breakfast taco" is "egg breakfast taco", never "potato" (AGL-4065).
1590
2281
  */ export function aiStockSubjectWords(alt, sectionName) {
1591
2282
  // "Hand-poured", "by hand" and "hands" say how it was made, never what it shows.
1592
- for (const clause of withoutHands(alt).split(CLAUSE_BREAK)){
1593
- const found = subjectOf(contentWords(clause != null ? clause : ''));
1594
- if (found.length) return found.slice(-3).join(' ');
2283
+ const text = withoutHands(alt);
2284
+ const breaker = new RegExp(CLAUSE_BREAK.source, 'g');
2285
+ const clauses = [];
2286
+ let at = 0;
2287
+ for (const match of text.matchAll(breaker)){
2288
+ var _match_index;
2289
+ clauses.push({
2290
+ words: subjectOf(contentWords(text.slice(at, match.index))),
2291
+ then: match[0].trim()
2292
+ });
2293
+ at = ((_match_index = match.index) != null ? _match_index : 0) + match[0].length;
2294
+ }
2295
+ clauses.push({
2296
+ words: subjectOf(contentWords(text.slice(at))),
2297
+ then: ''
2298
+ });
2299
+ for (const [index, { words: found, then }] of clauses.entries()){
2300
+ var _ref;
2301
+ var _clauses_;
2302
+ if (!found.length) continue;
2303
+ // Never where the second is one thing in a wrapper: "Soap and Lotion Gift Set" is a gift of both.
2304
+ const next = (_ref = (_clauses_ = clauses[index + 1]) == null ? void 0 : _clauses_.words) != null ? _ref : [];
2305
+ const things = next.filter((word)=>!WRAPPERS.has(word));
2306
+ if ((then === 'and' || then === '&') && found.length <= 2 && things.length >= 2) return next.slice(-3).join(' ');
2307
+ return found.slice(-3).join(' ');
1595
2308
  }
1596
2309
  return subjectOf(contentWords(withoutHands(sectionName))).slice(0, 2).join(' ');
1597
2310
  }
@@ -2550,7 +3263,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2550
3263
  * asks first for the role its words or its list's name (`sectionName`) say,
2551
3264
  * in the site's world: "teacher classroom" for a "Math instructor" (AGL-3856).
2552
3265
  */ export function aiStockSearchesFor(slot, terms, subject, part, sectionName = '') {
2553
- var _terms_core, _terms_making, _ref, _ref1, _terms_setting, _terms_setting1, _ref2, _terms_maker, _terms_maker1, _ref3;
3266
+ var _terms_core, _terms_leadWord, _terms_making, _ref, _ref1, _terms_setting, _terms_setting1, _ref2, _terms_maker, _terms_maker1, _ref3;
2554
3267
  var _aiStockPeopleRole, _slot_record, _slot_record1;
2555
3268
  const orientation = aiStockOrientation(slot.aspect);
2556
3269
  const size = slot.role === 'hero' ? {
@@ -2596,7 +3309,16 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2596
3309
  // A hero of a craft names the craft itself: the Saltmarsh Soap Co hero
2597
3310
  // (2026-10-10) was a florist's bouquet tagged "natural" (AGL-3660).
2598
3311
  const core = (_terms_core = terms.core) != null ? _terms_core : [];
2599
- const qualify = (phrase)=>terms.craft && !namesWorld && !stemList(phrase).includes(terms.craft) ? `${terms.craft} ${phrase}` : phrase;
3312
+ // The world a trade serves qualifies its pictures where the craft is the trade: "dirt bike", never "part" (AGL-4065).
3313
+ const leadWord = (_terms_leadWord = terms.leadWord) != null ? _terms_leadWord : terms.craft;
3314
+ const leadStems = stemList(leadWord);
3315
+ const qualify = (phrase)=>leadWord && !namesWorld && !leadStems.every((stem)=>stemList(phrase).includes(stem)) ? `${leadWord} ${phrase}` : phrase;
3316
+ // A part, gear, a machine, or one word of a two-word world ("bike" of "dirt
3317
+ // bike"), is never searched alone (AGL-4065): searched with the world, and
3318
+ // its photo is OF the thing and names the world. "Parts for the ranch"
3319
+ // searched "parts" and took a canoe tagged "part".
3320
+ const ofTheWorld = Boolean(leadWord) && subjectStems.length > 0 && subjectStems.every((stem)=>WORLD_OBJECTS.has(stem) || leadStems.includes(stem)) && (subjectStems.some((stem)=>WORLD_OBJECTS.has(stem)) || leadStems.length > 1 && subjectStems.length < leadStems.length);
3321
+ /** The words a photo of the world names, the world's things left out: a dirt bike's, never "part". */ const worldNames = core.filter((stem)=>!stem.split(' ').some((word)=>WORLD_OBJECTS.has(word)));
2600
3322
  // A collection tile is product-like: held to the shop's category and its
2601
3323
  // own object among the first tags, and searched by its object's kin, never
2602
3324
  // the category's world alone (AGL-3660).
@@ -2627,21 +3349,51 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2627
3349
  }
2628
3350
  // The work's own subject is never qualified by the craft: "getting ready", never "photographer getting ready".
2629
3351
  const own = work ? (phrase)=>phrase : qualify;
2630
- const thing = (subject ? [
3352
+ // A record's cover is OF what it names, searched in the site's world first
3353
+ // (AGL-4065): "handyman drywall" before "drywall", which took an architect's
3354
+ // house with a family before its studs.
3355
+ const record = Boolean(slot.record) && !work && !ofTheWorld && Boolean(leadWord);
3356
+ const thing = (!subject ? [] : ofTheWorld ? [
2631
3357
  {
2632
- query: own(subject),
2633
- held: true
3358
+ query: `${leadWord} ${subjectStems.some((stem)=>!leadStems.includes(stem)) ? subject.split(/\s+/).filter((word)=>!leadStems.includes(aiStockStem(word))).join(' ') : ''}`.trim(),
3359
+ held: true,
3360
+ leads: true,
3361
+ ofWorld: true
2634
3362
  },
2635
3363
  ...kin ? kin.queries.map((query)=>({
2636
3364
  query,
2637
- held: true
2638
- })) : [
3365
+ held: true,
3366
+ leads: true,
3367
+ ofWorld: true
3368
+ })) : []
3369
+ ] : [
3370
+ ...record && !leadStems.every((stem)=>subjectStems.includes(stem)) ? [
2639
3371
  {
3372
+ query: `${leadWord} ${subject}`,
3373
+ held: true,
3374
+ leads: true
3375
+ }
3376
+ ] : [],
3377
+ _extends({
3378
+ query: own(subject),
3379
+ held: true
3380
+ }, record ? {
3381
+ leads: true
3382
+ } : {}),
3383
+ ...kin ? kin.queries.map((query)=>_extends({
3384
+ query,
3385
+ held: true
3386
+ }, record ? {
3387
+ leads: true
3388
+ } : {})) : [
3389
+ _extends({
2640
3390
  query: own(object),
2641
3391
  held: true
2642
- }
3392
+ }, record ? {
3393
+ leads: true
3394
+ } : {})
2643
3395
  ]
2644
- ] : []).filter((query)=>said(query.query));
3396
+ ]).filter((query)=>said(query.query));
2645
3397
  // Never the maker, the camera, an animal or a toy, unless the record names it.
2646
3398
  const recordNamed = work ? new Set(stemList(`${slot.alt} ${(_ref2 = (_slot_record1 = slot.record) == null ? void 0 : _slot_record1.category) != null ? _ref2 : ''}`)) : null;
2647
3399
  const refuseLead = recordNamed ? [
@@ -2795,16 +3547,18 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2795
3547
  broad: true
2796
3548
  } : {}, held && kinPhrases.length ? {
2797
3549
  kin: kinPhrases
2798
- } : {}, held && collection ? {
3550
+ } : {}, held && collection || raw.leads ? {
2799
3551
  lead: true
2800
- } : {}, terms.craft && (raw.category || raw.held && collection) ? _extends({
3552
+ } : {}, terms.craft && !raw.ofWorld && (raw.category || raw.held && collection) ? _extends({
2801
3553
  requires: terms.craft
2802
3554
  }, ownWorld.length ? {
2803
3555
  category: [
2804
3556
  terms.craft,
2805
3557
  ...ownWorld
2806
3558
  ]
2807
- } : {}) : {}, slot.role === 'hero' && core.length ? {
3559
+ } : {}) : {}, raw.ofWorld && worldNames.length ? {
3560
+ category: worldNames
3561
+ } : {}, slot.role === 'hero' && core.length ? {
2808
3562
  category: core
2809
3563
  } : {}, slot.role === 'hero' ? {
2810
3564
  first: true
@@ -2849,15 +3603,17 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2849
3603
  var _photo_tags, _photo_alt, _photo_alt1, _tags_, _photo_alt2;
2850
3604
  const slug = pageSlug(photo.pageUrl);
2851
3605
  const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
2852
- const lead = new Set(stemList([
3606
+ // Each tag's words and pairs of them, read tag by tag (AGL-4065): "race car"
3607
+ // and "dirt bike" are phrases a world names; the page address is read whole.
3608
+ const lead = termsOf([
2853
3609
  ...tags.slice(0, 3),
2854
3610
  slug
2855
- ].join(' ')));
2856
- const all = new Set(stemList([
3611
+ ]);
3612
+ const all = termsOf([
2857
3613
  ...tags,
2858
3614
  (_photo_alt = photo.alt) != null ? _photo_alt : '',
2859
3615
  slug
2860
- ].join(' ')));
3616
+ ]);
2861
3617
  const phrases = [
2862
3618
  ...tags.map((tag)=>stemList(tag).join(' ')),
2863
3619
  stemList((_photo_alt1 = photo.alt) != null ? _photo_alt1 : '').join(' '),
@@ -2891,6 +3647,11 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2891
3647
  */ export function aiStockSensitive(photo, named = []) {
2892
3648
  const { words: found } = hitWords(photo);
2893
3649
  const site = new Set(named);
3650
+ // A maker's name, in its tags or its page address, unless the site names the maker (AGL-4065).
3651
+ const makers = aiStockMakerTerms(photo);
3652
+ if ([
3653
+ ...MAKER_STEMS
3654
+ ].some((maker)=>makers.has(maker) && !site.has(maker))) return true;
2894
3655
  return SENSITIVE_STEMS.some((topic)=>[
2895
3656
  ...topic.words
2896
3657
  ].some((word)=>found.has(word)) && ![
@@ -2909,7 +3670,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2909
3670
  * site's world 1, plus a quarter when it is among the first tags.
2910
3671
  */ export function aiStockRelevance(photo, judgement) {
2911
3672
  var _judgement_rivals, _judgement_subject, _judgement_named, _judgement_shun, _judgement_refuse, _judgement_refuseLead, _judgement_needs, _judgement_shunLead, _judgement_leadAny, _judgement_kin, _judgement_requires, _judgement_category, _judgement_object, _judgement_domain, _judgement_roles;
2912
- var _judgement_roleScene, _judgement_roleRefuse, _judgement_away;
3673
+ var _judgement_roleScene, _judgement_roleRefuse, _judgement_away, _judgement_world;
2913
3674
  const hit = hitWords(photo);
2914
3675
  if (hit.rejected) return 0;
2915
3676
  if (aiStockSensitive(photo, judgement.named)) return 0;
@@ -2965,7 +3726,8 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2965
3726
  const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
2966
3727
  const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
2967
3728
  const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
2968
- const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).flatMap(stemList);
3729
+ // A two-word name stays whole: "dirt bike", never "bike" (AGL-4065).
3730
+ const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).map(stemPhrase).filter(Boolean);
2969
3731
  const anyOf = category.length ? [
2970
3732
  ...new Set([
2971
3733
  ...required,
@@ -2978,21 +3740,26 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2978
3740
  if (demanded && !kinNamed && !meets) return 0;
2979
3741
  const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
2980
3742
  const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
3743
+ // The business's own world, which a picture that is not of a thing names (AGL-4065).
3744
+ const own = ((_judgement_world = judgement.world) == null ? void 0 : _judgement_world.length) ? judgement.world.filter((stem)=>stem !== object) : domain;
2981
3745
  // A hit naming the word it must name is of the site's world: a product's
2982
3746
  // category is that world even where it is the product's own object.
2983
3747
  const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
3748
+ const ownWorld = demanded || kinNamed || own.some((stem)=>hit.all.has(stem));
2984
3749
  const worldLead = (anyOf.length ? anyOf.some((stem)=>hit.lead.has(stem)) : required.length > 0 && required.every((stem)=>hit.lead.has(stem))) || kinLead || domain.some((stem)=>hit.lead.has(stem));
2985
3750
  if (judgement.leadWorld && !worldLead) return 0;
2986
3751
  // The first tag alone, never the page's address: Pixabay's slug repeats a
2987
3752
  // late tag ("coffee-latte-art-candle-…"). A site whose words name no world
2988
3753
  // holds its hero to none.
3754
+ // The business's own world, never every word of its type (AGL-4065): "race
3755
+ // car" named "race" of "Amateur motocross race team: … race schedule".
2989
3756
  const worldStems = new Set([
2990
3757
  ...anyOf,
2991
3758
  ...required,
2992
- ...domain
3759
+ ...own
2993
3760
  ]);
2994
3761
  if (judgement.first && (worldStems.size || kin.length)) {
2995
- const firstWorld = hit.firstPhrase.split(' ').some((stem)=>worldStems.has(stem)) || kin.some((phrase)=>among([
3762
+ const firstWorld = withPairs(hit.firstPhrase.split(' ').filter(Boolean)).some((stem)=>worldStems.has(stem)) || kin.some((phrase)=>among([
2996
3763
  hit.firstPhrase
2997
3764
  ], phrase));
2998
3765
  if (!firstWorld) return 0;
@@ -3017,8 +3784,8 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
3017
3784
  if (firstNamed(kin.length ? kin : [
3018
3785
  object
3019
3786
  ])) score += 1;
3020
- } else if (domain.length) {
3021
- if (!world) return 0;
3787
+ } else if (own.length) {
3788
+ if (!ownWorld) return 0;
3022
3789
  }
3023
3790
  if (leadAny.length && firstNamed(leadAny)) score += 1;
3024
3791
  if (phrased) score += 3;
@@ -3032,7 +3799,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
3032
3799
  // attorney" over "lawyers, personal injury, accident" (AGL-3856): a second
3033
3800
  // of them naming its world says it is of that world, not of something else.
3034
3801
  if (judgement.first && worldStems.size) {
3035
- const said = hit.leadPhrases.slice(0, -1).filter((phrase)=>phrase.split(' ').some((stem)=>worldStems.has(stem)));
3802
+ const said = hit.leadPhrases.slice(0, -1).filter((phrase)=>withPairs(phrase.split(' ')).some((stem)=>worldStems.has(stem)));
3036
3803
  if (said.length >= 2) score += 0.25;
3037
3804
  }
3038
3805
  return Math.max(score, 0.5);
@@ -3217,7 +3984,8 @@ function jobPhotos(key) {
3217
3984
  /**
3218
3985
  * Makers' names a tag gives, never what a photo shows to someone who cannot
3219
3986
  * see it (AGL-3856): "A photo-camera and a sony".
3220
- */ const BRANDS = words('sony canon cannon nikon fujifilm fuji leica olympus panasonic kodak polaroid hasselblad pentax gopro dji ' + 'apple iphone ipad macbook imac samsung huawei android google microsoft windows dell lenovo nintendo playstation ' + 'xbox nike adidas puma reebok peloton bmw mercedes audi porsche ferrari lamborghini tesla ford toyota honda ' + 'volkswagen vw jeep chevrolet coca-cola pepsi starbucks mcdonalds ikea lego barbie disney altman ' + 'petri zenit praktica yashica minolta mamiya rolleiflex voigtlander contax ricoh konica lomo lomography holga zeiss');
3987
+ */ const BRANDS = words('sony canon cannon nikon fujifilm fuji leica olympus panasonic kodak polaroid hasselblad pentax gopro dji ' + 'apple iphone ipad macbook imac samsung huawei android google microsoft windows dell lenovo nintendo playstation ' + 'xbox nike adidas puma reebok peloton bmw mercedes audi porsche ferrari lamborghini tesla ford toyota honda ' + 'volkswagen vw jeep chevrolet coca-cola pepsi starbucks mcdonalds ikea lego barbie disney altman ' + // The vehicle makers a moto site's photos are tagged with (AGL-4065).
3988
+ 'yamaha kawasaki suzuki ktm ducati harley harley-davidson husqvarna gasgas nissan subaru mazda chevy dodge reebok ' + 'petri zenit praktica yashica minolta mamiya rolleiflex voigtlander contax ricoh konica lomo lomography holga zeiss');
3221
3989
  /** Stopwords a description still says, as the people a photo shows: "a family", "a person". */ const SAID_PEOPLE = words('people person family');
3222
3990
  /** Words ending in "-ing" that are things, and take an article: "a wedding", "a ring". */ const COUNTED_ING = words('wedding ring earring building painting drawing ceiling string swing king sibling wing dumpling pudding sapling');
3223
3991
  /** "-ing" words that are things, and take no article: "clothing". */ const UNCOUNTED_ING = words('clothing bedding frosting icing flooring stuffing seasoning filling');
@@ -4339,13 +5107,16 @@ const EXTENSIONS = {
4339
5107
  const base = aiStockSiteTerms(entry.subject);
4340
5108
  if (!((_entry_kin = entry.kin) == null ? void 0 : _entry_kin.length)) return base;
4341
5109
  const kin = entry.kin.map(aiStockStem);
5110
+ const domain = [
5111
+ ...new Set([
5112
+ ...base.domain,
5113
+ ...kin
5114
+ ])
5115
+ ];
4342
5116
  return _extends({}, base, {
4343
- domain: [
4344
- ...new Set([
4345
- ...base.domain,
4346
- ...kin
4347
- ])
4348
- ],
5117
+ domain,
5118
+ // The kind's own world is all of it: its subject and its kin (AGL-4065).
5119
+ lead: domain,
4349
5120
  broad: [
4350
5121
  ...(_entry_broad = entry.broad) != null ? _entry_broad : base.broad
4351
5122
  ],
@@ -4880,7 +5651,7 @@ const WORKSPACE = {
4880
5651
  * "freelencing", which no library tags, so its cover hero searched it, found
4881
5652
  * nothing, and shipped a flat dark band with no photo.
4882
5653
  */ export function aiStockWiderTerms(terms, kind) {
4883
- var _STARTER_KINDS_starter, _aiStockKindGenericTerms, _STARTER_GENERIC_KIND_starter;
5654
+ var _STARTER_KINDS_starter, _STARTER_GENERIC_KIND_starter;
4884
5655
  const rungs = [];
4885
5656
  const seen = new Set([
4886
5657
  JSON.stringify([
@@ -4890,8 +5661,11 @@ const WORKSPACE = {
4890
5661
  ]
4891
5662
  ])
4892
5663
  ]);
4893
- const add = (rung, found)=>{
5664
+ // An outdoor or a trade world never widens to an office's photos (AGL-4065).
5665
+ const outdoor = aiStockWorldIsOutdoorOrTrade(terms);
5666
+ const add = (rung, of, found)=>{
4894
5667
  if (!found || !found.broad.length) return;
5668
+ if (outdoor && OFFICE_KINDS.has(of)) return;
4895
5669
  const key = JSON.stringify([
4896
5670
  found.craft,
4897
5671
  found.broad
@@ -4904,15 +5678,42 @@ const WORKSPACE = {
4904
5678
  });
4905
5679
  };
4906
5680
  if (!aiStockWorldIsUnlisted(terms)) {
4907
- if (kind && worldOfKind(terms, kind)) add('generic', aiStockKindGenericTerms(kind));
5681
+ if (kind && worldOfKind(terms, kind)) add('generic', kind, aiStockKindGenericTerms(kind));
4908
5682
  return rungs;
4909
5683
  }
4910
5684
  const starter = starterOfKind(kind);
4911
- if (kind) add('kind', aiStockKindTerms(kind));
4912
- add('starter', aiStockKindTerms((_STARTER_KINDS_starter = STARTER_KINDS[starter]) != null ? _STARTER_KINDS_starter : 'business'));
4913
- add('generic', (_aiStockKindGenericTerms = aiStockKindGenericTerms(kind)) != null ? _aiStockKindGenericTerms : aiStockKindGenericTerms((_STARTER_GENERIC_KIND_starter = STARTER_GENERIC_KIND[starter]) != null ? _STARTER_GENERIC_KIND_starter : 'business'));
5685
+ const starterKind = (_STARTER_KINDS_starter = STARTER_KINDS[starter]) != null ? _STARTER_KINDS_starter : 'business';
5686
+ if (kind) add('kind', kind, aiStockKindTerms(kind));
5687
+ add('starter', starterKind, aiStockKindTerms(starterKind));
5688
+ const generic = KIND_GENERIC[kind] ? kind : (_STARTER_GENERIC_KIND_starter = STARTER_GENERIC_KIND[starter]) != null ? _STARTER_GENERIC_KIND_starter : 'business';
5689
+ add('generic', generic, aiStockKindGenericTerms(generic));
4914
5690
  return rungs;
4915
5691
  }
5692
+ /**
5693
+ * The kinds of site whose photos are an office, a desk or a workspace
5694
+ * (AGL-4065): what a landscaper, a roofer or a taco trailer never widens to.
5695
+ */ const OFFICE_KINDS = new Set([
5696
+ 'business',
5697
+ 'landing',
5698
+ 'professional',
5699
+ 'personal',
5700
+ 'coming-soon',
5701
+ 'studio'
5702
+ ]);
5703
+ /**
5704
+ * Words of a world whose work is done outdoors, on a vehicle, with tools or
5705
+ * over food (AGL-4065), as stems: such a world's pictures are never an
5706
+ * office's. The Bluebonnet Yard & Landscape start, a world the module did not
5707
+ * list, picked as Business & services, opened every page on an office.
5708
+ */ const OUTDOOR_AND_TRADE_STEMS = new Set(('landscaping landscaper landscape lawn garden gardening yard roof roofing roofer fence fencing deck patio construction ' + 'contractor farm ranch moto motorcycle motocross bike dirt atv truck trailer tow towing auto car mechanic food ' + 'restaurant kitchen taco plumbing plumber electrician electrical hvac painting painter handyman carpentry carpenter ' + 'tree pool pest cleaning junk excavation concrete masonry welding').split(' ').map(aiStockStem));
5709
+ /** Whether a site's own world is outdoors, on a vehicle, with tools or over food ({@link OUTDOOR_AND_TRADE_STEMS}). */ export function aiStockWorldIsOutdoorOrTrade(terms) {
5710
+ var _terms_lead;
5711
+ const own = [
5712
+ ...(_terms_lead = terms.lead) != null ? _terms_lead : terms.domain,
5713
+ ...stemList(`${terms.business} ${terms.craft}`)
5714
+ ];
5715
+ return own.some((term)=>term.split(' ').some((stem)=>OUTDOOR_AND_TRADE_STEMS.has(stem)));
5716
+ }
4916
5717
  /**
4917
5718
  * The first word of a text that names a craft {@link CRAFT_KIN} lists, as
4918
5719
  * its table writes it: "gym" of "Synchronicity Gym", "law" of "Hartwell
@@ -5159,10 +5960,13 @@ const WORKSPACE = {
5159
5960
  * (AGL-3923). What lets a sensitive topic in is always the site's own words.
5160
5961
  */ const judgementOf = (slot, request, subject, world)=>{
5161
5962
  var _request_subject, _request_shun;
5162
- var _request_allow, _request_shun1, _request_leadAny, _request_refuse, _request_needs, _request_refuseLead, _request_away;
5963
+ var _world_lead, _request_allow, _request_shun1, _request_leadAny, _request_refuse, _request_needs, _request_refuseLead, _request_away;
5163
5964
  return _extends({
5164
5965
  subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
5165
- domain: world.domain,
5966
+ domain: world.domain
5967
+ }, ((_world_lead = world.lead) == null ? void 0 : _world_lead.length) ? {
5968
+ world: world.lead
5969
+ } : {}, {
5166
5970
  strict: slot.role === 'gallery' && !request.broad || !!request.requires || !!request.strict,
5167
5971
  named: ((_request_allow = request.allow) == null ? void 0 : _request_allow.length) ? [
5168
5972
  ...new Set([
@@ -5331,9 +6135,11 @@ const WORKSPACE = {
5331
6135
  }) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName), sectionName),
5332
6136
  ...craftSearches(slot)
5333
6137
  ];
6138
+ /** Whether the library answered any of the slot's own searches with a photo at all (AGL-4065). */ let knew = false;
5334
6139
  /** The slot's accepted hits for a search; `null` once the page's searches are spent. */ const accepted = async (request, repeat)=>{
5335
6140
  const found = await search(request);
5336
6141
  if (found === null) return null;
6142
+ if (found.length && !worldOf.has(request)) knew = true;
5337
6143
  const blocked = new Set();
5338
6144
  if (!repeat) {
5339
6145
  for (const key of claimed)blocked.add(key);
@@ -5383,44 +6189,54 @@ const WORKSPACE = {
5383
6189
  const sure = slot.product ? requests.filter((request)=>request.tier !== 'weak') : requests;
5384
6190
  const unsure = slot.product ? requests.filter((request)=>request.tier === 'weak') : [];
5385
6191
  // 3. Nothing of its own world at all: the wider worlds (AGL-3923), new
5386
- // and then, for a hero, one the job already shows.
6192
+ // and then, for a hero, one the job already shows. A listed world
6193
+ // widens only where the library knew nothing of it (AGL-4065): one
6194
+ // it answered with photos it turned down keeps its slot without a
6195
+ // photo, never an office. A world of its own words alone still widens.
5387
6196
  const widened = widerSearches(slot, subject, sectionName);
5388
6197
  const phases = [
5389
6198
  [
5390
6199
  sure,
6200
+ false,
5391
6201
  false
5392
6202
  ],
5393
6203
  ...repeats ? [
5394
6204
  [
5395
6205
  sure,
5396
- true
6206
+ true,
6207
+ false
5397
6208
  ]
5398
6209
  ] : [],
5399
6210
  ...unsure.length ? [
5400
6211
  [
5401
6212
  unsure,
6213
+ false,
5402
6214
  false
5403
6215
  ],
5404
6216
  [
5405
6217
  unsure,
5406
- true
6218
+ true,
6219
+ false
5407
6220
  ]
5408
6221
  ] : [],
5409
6222
  ...widened.length ? [
5410
6223
  [
5411
6224
  widened,
5412
- false
6225
+ false,
6226
+ true
5413
6227
  ]
5414
6228
  ] : [],
5415
6229
  ...widened.length && repeats ? [
5416
6230
  [
5417
6231
  widened,
6232
+ true,
5418
6233
  true
5419
6234
  ]
5420
6235
  ] : []
5421
6236
  ];
5422
- for (const [tried, repeat] of phases){
6237
+ for (const [tried, repeat, wide] of phases){
5423
6238
  if (choice || signal.aborted) break;
6239
+ if (wide && knew && !unlisted) break;
5424
6240
  choice = await choose(tried, repeat);
5425
6241
  }
5426
6242
  choices.push(choice);