@aglyn/plugins-ai 1.0.0-beta.285 → 1.0.0-beta.288

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 (228) 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-business-profile-prefill.js +6 -3
  16. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -1
  17. package/src/lib/jobs/ai-image-reads.d.ts +59 -0
  18. package/src/lib/jobs/ai-image-reads.js +138 -0
  19. package/src/lib/jobs/ai-image-reads.js.map +1 -0
  20. package/src/lib/jobs/ai-job-build-step.d.ts +29 -0
  21. package/src/lib/jobs/ai-job-build-step.js +83 -12
  22. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  23. package/src/lib/jobs/ai-job-duplicate-step.d.ts +2 -1
  24. package/src/lib/jobs/ai-job-duplicate-step.js +6 -6
  25. package/src/lib/jobs/ai-job-duplicate-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-import-step.d.ts +4 -2
  27. package/src/lib/jobs/ai-job-import-step.js +15 -11
  28. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-insight-step.d.ts +10 -0
  30. package/src/lib/jobs/ai-job-insight-step.js +23 -5
  31. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  32. package/src/lib/jobs/ai-job-layout-language.js +2 -1
  33. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  34. package/src/lib/jobs/ai-job-media-step.d.ts +2 -1
  35. package/src/lib/jobs/ai-job-media-step.js +6 -8
  36. package/src/lib/jobs/ai-job-media-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-navigation-step.d.ts +2 -1
  38. package/src/lib/jobs/ai-job-navigation-step.js +7 -9
  39. package/src/lib/jobs/ai-job-navigation-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-page-language.js +7 -1
  41. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  42. package/src/lib/jobs/ai-job-page-sections.js +16 -3
  43. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  44. package/src/lib/jobs/ai-job-page-step.js +9 -1
  45. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  46. package/src/lib/jobs/ai-job-plan-step.d.ts +30 -8
  47. package/src/lib/jobs/ai-job-plan-step.js +78 -13
  48. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-privacy-step.d.ts +4 -2
  50. package/src/lib/jobs/ai-job-privacy-step.js +14 -15
  51. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-products-step.js +5 -3
  53. package/src/lib/jobs/ai-job-products-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-settings-step.d.ts +2 -1
  55. package/src/lib/jobs/ai-job-settings-step.js +7 -8
  56. package/src/lib/jobs/ai-job-settings-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +34 -1
  58. package/src/lib/jobs/ai-job-site-content.js +67 -8
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-datasets.d.ts +53 -3
  61. package/src/lib/jobs/ai-job-site-datasets.js +300 -30
  62. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -1
  63. package/src/lib/jobs/ai-job-site-step.js +6 -3
  64. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-store-pages.js +3 -3
  66. package/src/lib/jobs/ai-job-site-store-pages.js.map +1 -1
  67. package/src/lib/jobs/ai-job-text-step.js +2 -1
  68. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  69. package/src/lib/jobs/ai-job-tracking-step.d.ts +2 -1
  70. package/src/lib/jobs/ai-job-tracking-step.js +7 -8
  71. package/src/lib/jobs/ai-job-tracking-step.js.map +1 -1
  72. package/src/lib/jobs/ai-jobs-beat.d.ts +11 -7
  73. package/src/lib/jobs/ai-jobs-beat.js +18 -8
  74. package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
  75. package/src/lib/jobs/ai-jobs-notify.d.ts +66 -1
  76. package/src/lib/jobs/ai-jobs-notify.js +136 -3
  77. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  78. package/src/lib/jobs/ai-layout-stock-photos.d.ts +155 -7
  79. package/src/lib/jobs/ai-layout-stock-photos.js +1517 -220
  80. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  81. package/src/lib/jobs/ai-product-image.d.ts +14 -0
  82. package/src/lib/jobs/ai-product-image.js +73 -21
  83. package/src/lib/jobs/ai-product-image.js.map +1 -1
  84. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +9 -1
  85. package/src/lib/jobs/ai-theme-brand-inputs.js +28 -2
  86. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  87. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.d.ts +175 -0
  88. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js +601 -0
  89. package/src/lib/jobs/fixtures/ai-site-agl4065-datasets.js.map +1 -0
  90. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.d.ts +46 -0
  91. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js +2862 -0
  92. package/src/lib/jobs/fixtures/ai-site-agl4065-pages.js.map +1 -0
  93. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.d.ts +74 -0
  94. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js +888 -0
  95. package/src/lib/jobs/fixtures/ai-site-agl4065-plans.js.map +1 -0
  96. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.d.ts +120 -0
  97. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js +410 -0
  98. package/src/lib/jobs/fixtures/ai-site-agl4092-datasets.js.map +1 -0
  99. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.d.ts +29 -0
  100. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js +81 -0
  101. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-judge.js.map +1 -0
  102. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.d.ts +87 -0
  103. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js +225 -0
  104. package/src/lib/jobs/fixtures/ai-stock-photo-agl4065-recordings.js.map +1 -0
  105. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.d.ts +26 -0
  106. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js +714 -0
  107. package/src/lib/jobs/fixtures/ai-stock-photo-agl4092-recordings.js.map +1 -0
  108. package/src/lib/layout-language/ai-layout-compiler.d.ts +11 -0
  109. package/src/lib/layout-language/ai-layout-compiler.js +194 -18
  110. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  111. package/src/lib/layout-language/ai-layout-copy.d.ts +37 -0
  112. package/src/lib/layout-language/ai-layout-copy.js +383 -3
  113. package/src/lib/layout-language/ai-layout-copy.js.map +1 -1
  114. package/src/lib/layout-language/ai-layout-design.d.ts +19 -0
  115. package/src/lib/layout-language/ai-layout-design.js +31 -11
  116. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  117. package/src/lib/layout-language/ai-layout-listings.d.ts +7 -1
  118. package/src/lib/layout-language/ai-layout-listings.js +9 -1
  119. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  120. package/src/lib/layout-language/ai-layout-pictures.d.ts +39 -1
  121. package/src/lib/layout-language/ai-layout-pictures.js +141 -4
  122. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  123. package/src/lib/layout-language/ai-layout-storefront.d.ts +18 -4
  124. package/src/lib/layout-language/ai-layout-storefront.js +100 -20
  125. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -1
  126. package/src/lib/model/ai-build-job.d.ts +7 -0
  127. package/src/lib/model/ai-build-job.js +36 -7
  128. package/src/lib/model/ai-build-job.js.map +1 -1
  129. package/src/lib/model/ai-build-plan.d.ts +28 -4
  130. package/src/lib/model/ai-build-plan.js +49 -9
  131. package/src/lib/model/ai-build-plan.js.map +1 -1
  132. package/src/lib/model/ai-build-progress.js +3 -1
  133. package/src/lib/model/ai-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-job-notice.d.ts +8 -0
  135. package/src/lib/model/ai-job-notice.js +33 -0
  136. package/src/lib/model/ai-job-notice.js.map +1 -1
  137. package/src/lib/model/ai-job-plan-lines.d.ts +2 -1
  138. package/src/lib/model/ai-job-plan-lines.js +6 -1
  139. package/src/lib/model/ai-job-plan-lines.js.map +1 -1
  140. package/src/lib/model/ai-jobs.types.d.ts +8 -0
  141. package/src/lib/model/ai-jobs.types.js.map +1 -1
  142. package/src/lib/model/ai-permissions.d.ts +13 -0
  143. package/src/lib/model/ai-permissions.js +29 -1
  144. package/src/lib/model/ai-permissions.js.map +1 -1
  145. package/src/lib/model/ai-plan-said-sections.d.ts +12 -3
  146. package/src/lib/model/ai-plan-said-sections.js +22 -5
  147. package/src/lib/model/ai-plan-said-sections.js.map +1 -1
  148. package/src/lib/model/ai-products.d.ts +18 -0
  149. package/src/lib/model/ai-products.js +32 -0
  150. package/src/lib/model/ai-products.js.map +1 -1
  151. package/src/lib/model/ai-site-context.d.ts +6 -2
  152. package/src/lib/model/ai-site-context.js +8 -2
  153. package/src/lib/model/ai-site-context.js.map +1 -1
  154. package/src/lib/model/ai-site-inspiration.d.ts +3 -2
  155. package/src/lib/model/ai-site-inspiration.js +3 -2
  156. package/src/lib/model/ai-site-inspiration.js.map +1 -1
  157. package/src/lib/plugin.js +4 -13
  158. package/src/lib/plugin.js.map +1 -1
  159. package/src/lib/providers/anthropic.js +9 -2
  160. package/src/lib/providers/anthropic.js.map +1 -1
  161. package/src/lib/providers/catalog.js +14 -5
  162. package/src/lib/providers/catalog.js.map +1 -1
  163. package/src/lib/providers/contract.d.ts +24 -1
  164. package/src/lib/providers/contract.js.map +1 -1
  165. package/src/lib/providers/routing.js +5 -2
  166. package/src/lib/providers/routing.js.map +1 -1
  167. package/src/lib/runtime/ai-dataset-generation.d.ts +120 -1
  168. package/src/lib/runtime/ai-dataset-generation.js +352 -3
  169. package/src/lib/runtime/ai-dataset-generation.js.map +1 -1
  170. package/src/lib/runtime/ai-dev-replay.d.ts +1 -0
  171. package/src/lib/runtime/ai-dev-replay.js +4 -2
  172. package/src/lib/runtime/ai-dev-replay.js.map +1 -1
  173. package/src/lib/runtime/ai-doctrine-validators.d.ts +47 -3
  174. package/src/lib/runtime/ai-doctrine-validators.js +345 -28
  175. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  176. package/src/lib/runtime/ai-doctrine.d.ts +5 -4
  177. package/src/lib/runtime/ai-doctrine.js +10 -3
  178. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  179. package/src/lib/runtime/ai-icon-library.d.ts +27 -1
  180. package/src/lib/runtime/ai-icon-library.js +30 -3
  181. package/src/lib/runtime/ai-icon-library.js.map +1 -1
  182. package/src/lib/runtime/ai-products-generation.d.ts +11 -1
  183. package/src/lib/runtime/ai-products-generation.js +51 -4
  184. package/src/lib/runtime/ai-products-generation.js.map +1 -1
  185. package/src/lib/runtime/assist-answer-cache.d.ts +2 -0
  186. package/src/lib/runtime/assist-answer-cache.js +5 -2
  187. package/src/lib/runtime/assist-answer-cache.js.map +1 -1
  188. package/src/lib/server/ai-allotments.js +7 -2
  189. package/src/lib/server/ai-allotments.js.map +1 -1
  190. package/src/lib/server/ai-experiment-versions.js +5 -5
  191. package/src/lib/server/ai-experiment-versions.js.map +1 -1
  192. package/src/lib/server/ai-host-permissions-roster.d.ts +25 -0
  193. package/src/lib/server/ai-host-permissions-roster.js +33 -0
  194. package/src/lib/server/ai-host-permissions-roster.js.map +1 -0
  195. package/src/lib/server/ai-host-permissions.js +8 -6
  196. package/src/lib/server/ai-host-permissions.js.map +1 -1
  197. package/src/lib/server/ai-image-reads-route.d.ts +17 -0
  198. package/src/lib/server/ai-image-reads-route.js +106 -0
  199. package/src/lib/server/ai-image-reads-route.js.map +1 -0
  200. package/src/lib/server/ai-seo-apply.js +5 -5
  201. package/src/lib/server/ai-seo-apply.js.map +1 -1
  202. package/src/lib/server/ai-site-import-route.js +7 -4
  203. package/src/lib/server/ai-site-import-route.js.map +1 -1
  204. package/src/lib/server/assist-build.d.ts +15 -1
  205. package/src/lib/server/assist-build.js +30 -3
  206. package/src/lib/server/assist-build.js.map +1 -1
  207. package/src/lib/server/assist-chat.d.ts +11 -7
  208. package/src/lib/server/assist-chat.js +185 -48
  209. package/src/lib/server/assist-chat.js.map +1 -1
  210. package/src/lib/server/assist-docs-query.d.ts +51 -0
  211. package/src/lib/server/assist-docs-query.js +57 -0
  212. package/src/lib/server/assist-docs-query.js.map +1 -0
  213. package/src/lib/server/assist-jobs-context.d.ts +102 -0
  214. package/src/lib/server/assist-jobs-context.js +227 -0
  215. package/src/lib/server/assist-jobs-context.js.map +1 -0
  216. package/src/lib/server/assist-view-context.d.ts +8 -4
  217. package/src/lib/server/assist-view-context.js +12 -7
  218. package/src/lib/server/assist-view-context.js.map +1 -1
  219. package/src/lib/server.js +12 -0
  220. package/src/lib/server.js.map +1 -1
  221. package/src/lib/tools/ai-dataset-tool.d.ts +11 -1
  222. package/src/lib/tools/ai-dataset-tool.js +24 -7
  223. package/src/lib/tools/ai-dataset-tool.js.map +1 -1
  224. package/src/lib/tools/ai-duplicate-resource-tool.js +5 -6
  225. package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
  226. package/src/lib/components/ai-collaborator-permissions-column.component.d.ts +0 -32
  227. package/src/lib/components/ai-collaborator-permissions-column.component.js +0 -132
  228. package/src/lib/components/ai-collaborator-permissions-column.component.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
2
3
  /**
3
4
  * @license
4
5
  * Copyright 2026 Aglyn LLC
@@ -140,6 +141,36 @@ import { aiStockBusinessNoun } from "./ai-stock-business-nouns.js";
140
141
  * where this module lists it ("gym" of "Synchronicity Gym"); and a
141
142
  * training word's other senses (a railway's train, an elevator's lift, a
142
143
  * newspaper's press, a boat's row) never fill a gym's picture.
144
+ * 10. **The trade, never a word of it** (the 2026-10-25 booth kit, AGL-4065:
145
+ * six trades whose photos were race cars, cyclists, horses, a Honda
146
+ * scooter, a canoe for "Parts for the ranch", hotel buffets for a taco
147
+ * trailer and an office for a landscaper). A business named by two
148
+ * things ("Motorcycle and dirt bike repair shop") is both; the craft
149
+ * named first leads, never one named only in what a customer may ask
150
+ * ("takes catering requests"); a hero's first tag and a broad search's
151
+ * hit name the business's own world ({@link AiStockSiteTerms.lead}),
152
+ * never any word of its type ("race" of "race car"); a two-word world
153
+ * ("dirt bike") is named whole; a maker's name never shows; a sentence to
154
+ * the reader, a job or an order is never a subject, and a part or gear is
155
+ * searched only with its world; a record's cover is searched in its world
156
+ * first; and a listed world widens only where the library knew nothing of
157
+ * it, never an outdoor or a trade world to an office.
158
+ * 11. **The thing, the place and the trade** (the 2026-10-28 live check,
159
+ * AGL-4092: a bare foot tagged "body part" for a parts shop's "Engine and
160
+ * air", a transmission chain for its "Brake Pad Set", a cat tagged "hungry"
161
+ * for a taco trailer's "Come hungry", a European alley for "Around town", a
162
+ * taco bowl salad for "Race day", and a man posing with a street bike tagged
163
+ * "susuki" in a motocross team's band of pictures). A picture of a thing is
164
+ * held to the business's own world, never any word of its type; a feeling,
165
+ * a time or a way of being somewhere is never a thing, and "around town" is
166
+ * the business's customers out there; a broad search's photo is OF the
167
+ * world among its first tags, never a "taco bowl"; an animal, a toy or a
168
+ * bare body part never leads a photo naming nothing of the world; a trade's
169
+ * things are held to the world it serves, never to "part"; a product takes
170
+ * a shoot no other product shows; a motocross business's world is
171
+ * off-road; and a maker is named one letter off ("susuki") and by its
172
+ * gear's and tires' names. Still by the tags: a mark only the pixels show
173
+ * is placed.
143
174
  *
144
175
  * A slot the library could not fill because it never answered — down, or
145
176
  * out of time before its first answer — or because the site's library
@@ -211,7 +242,80 @@ import { aiStockBusinessNoun } from "./ai-stock-business-nouns.js";
211
242
  });
212
243
  }
213
244
  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');
245
+ /** 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
246
+ // the bike and we will say if it will fit" searched "tell us bike".
247
+ 'we us you they them i me if tell say describe send ask need want get let know see find will would could');
248
+ /**
249
+ * Words of a sentence to the reader that a business type may use as a name
250
+ * ("IT support", "Fit club", "call center"): never a picture's subject
251
+ * (AGL-4065), always a business's own word.
252
+ */ const SENTENCE_WORDS = words('it fit can call');
253
+ /**
254
+ * Nouns that never stand alone as what a picture shows (AGL-4065): a job, a
255
+ * request, an order. "Describe your job" searched "handyman describe job" and
256
+ * placed a job application form; "OEM ATV Parts Order" searched "atv parts
257
+ * order" and placed trees tagged "order".
258
+ */ const GENERIC_OBJECTS = words('job jobs request requests order orders service services detail details thing things stuff action cta');
259
+ /**
260
+ * Things a world has many of, which a photo shows only in that world
261
+ * (AGL-4065): a part, gear, a machine. Searched only with the site's own
262
+ * world ("dirt bike parts"), never alone: "parts" answered a canoe on a
263
+ * river, tagged "part", for a dirt bike shop's "Parts for the ranch".
264
+ */ const WORLD_OBJECTS = words('part parts gear gears machine machines equipment spare spares accessory accessories');
265
+ /**
266
+ * Words of a picture's caption that are never the thing it shows (AGL-4092):
267
+ * a feeling, a time, a way of being somewhere. "Come hungry" read its object
268
+ * as "hungry" and took a cat tagged "hungry"; "Around town" read "town" and
269
+ * took a European alley. A caption left with no thing is a picture of the
270
+ * site's own world. A personal record is a feat, never a thing: "Deadlift PR
271
+ * night" is of the deadlift.
272
+ */ const NON_THINGS = words('come hungry happy ready welcome day days night nights time times today tonight weekend weekends season seasons ' + 'around town hours hour way moment moments memories pr pb');
273
+ /**
274
+ * Where a business is, said as a place to go (AGL-4092): "Around town", "At
275
+ * events". Never a street of houses: a picture of it shows the business's
276
+ * customers out where it is, or its stand.
277
+ */ const PLACE_PHRASES = /\b(?:around town|in town|downtown|on the road|out and about|at (?:the )?events|neighbou?rhoods?)\b/i;
278
+ /** What a picture of a business out where it is shows, as stems: its people, a line, a street stand (AGL-4092). */ const PLACE_NEEDS = [
279
+ 'people',
280
+ 'crowd',
281
+ 'customer',
282
+ 'customers',
283
+ 'queue',
284
+ 'line',
285
+ 'street food',
286
+ 'food truck',
287
+ 'market',
288
+ 'festival'
289
+ ];
290
+ /** A street that is someone else's, never where a business is found (AGL-4092): a European alley for "Around town". */ const FOREIGN_STREET = [
291
+ 'alley',
292
+ 'cobblestone',
293
+ 'village',
294
+ 'old town',
295
+ 'historic',
296
+ 'castle',
297
+ 'cathedral',
298
+ 'europe',
299
+ 'landscape',
300
+ 'architecture'
301
+ ];
302
+ /**
303
+ * The verbs a business type's clause opens with when it says what a customer
304
+ * may ask of it (AGL-4065): "takes quote requests", "books repair
305
+ * appointments". The verb is never the business's world.
306
+ */ const TRANSACTION_VERBS = words('take takes taking book books booking accept accepts accepting welcome welcomes');
307
+ /**
308
+ * What a business does with a customer, never what its photos show
309
+ * (AGL-4065), as stems: dropped from its world. "Special orders" named
310
+ * Berm & Bolt's world, so a delivery man's "order" and trees tagged "order"
311
+ * filled its products.
312
+ */ 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.
313
+ 'booking appointment order request quote estimate free inquiry pickup special around town'.split(' '));
314
+ /**
315
+ * Words of a business's own clause that name its world only beside another
316
+ * (AGL-4065): "race" and "team" of "amateur motocross race team" are a race
317
+ * car's and an office's as often. Never its own world alone.
318
+ */ 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
319
  /**
216
320
  * Words that describe a picture's look, not what it shows, dropped from a
217
321
  * subject. A word that is as often a noun ("light", "set") is not one.
@@ -547,6 +651,165 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
547
651
  ].map(aiStockStem))
548
652
  };
549
653
  });
654
+ /**
655
+ * Makers whose names a photo's tags or its page address give, which no
656
+ * site's pictures carry unless its own words name the maker (AGL-4065): the
657
+ * Rut Runners Racing heroes were race cars in a pit lane under their makers'
658
+ * logos, and Holeshot Moto Repair's a Honda scooter and an Indian cruiser. A
659
+ * name that is also a common word ("apple", "puma", "triumph", "indian") is
660
+ * named only in the phrase that makes it the maker's. Read from the tags and
661
+ * the address, so a logo no tag names still shows: the live check is where
662
+ * that is seen.
663
+ */ /** The one-word makers of riding gear and tires (AGL-4092), also never said in a photo's description ({@link BRANDS}). */ const GEAR_MAKER_WORDS = [
664
+ 'alpinestars',
665
+ 'oneal',
666
+ 'leatt',
667
+ 'arai',
668
+ 'shoei',
669
+ 'agv',
670
+ 'oakley',
671
+ 'renthal',
672
+ 'fmf',
673
+ 'acerbis',
674
+ 'dunlop',
675
+ 'michelin',
676
+ 'pirelli',
677
+ 'bridgestone',
678
+ 'metzeler',
679
+ 'maxxis',
680
+ 'kenda'
681
+ ];
682
+ const MAKERS = [
683
+ 'porsche',
684
+ 'ferrari',
685
+ 'lamborghini',
686
+ 'bmw',
687
+ 'mercedes',
688
+ 'audi',
689
+ 'volkswagen',
690
+ 'vw',
691
+ 'toyota',
692
+ 'honda',
693
+ 'yamaha',
694
+ 'kawasaki',
695
+ 'suzuki',
696
+ 'ktm',
697
+ 'ducati',
698
+ 'harley',
699
+ 'harley-davidson',
700
+ 'husqvarna',
701
+ 'gasgas',
702
+ 'tesla',
703
+ 'ford',
704
+ 'chevrolet',
705
+ 'chevy',
706
+ 'dodge',
707
+ 'jeep',
708
+ 'nissan',
709
+ 'subaru',
710
+ 'mazda',
711
+ 'indian motorcycle',
712
+ 'triumph motorcycle',
713
+ 'gran turismo',
714
+ 'playstation',
715
+ 'xbox',
716
+ 'nintendo',
717
+ 'nike',
718
+ 'adidas',
719
+ 'reebok',
720
+ 'red bull',
721
+ 'monster energy',
722
+ 'coca cola',
723
+ 'cocacola',
724
+ 'pepsi',
725
+ 'iphone',
726
+ 'ipad',
727
+ 'macbook',
728
+ 'imac',
729
+ 'samsung',
730
+ 'starbucks',
731
+ 'mcdonalds',
732
+ 'mcdonald',
733
+ // A rider's gear and a bike's tires carry their makers' marks as often as
734
+ // the bike does (AGL-4092). A maker whose name is a common word ("fox",
735
+ // "shift", "thor", "fly", "bell") is named only in its phrase.
736
+ ...GEAR_MAKER_WORDS,
737
+ 'fox racing',
738
+ 'shift mx',
739
+ 'shift racing',
740
+ 'thor mx',
741
+ 'fly racing',
742
+ 'troy lee',
743
+ 'bell helmets',
744
+ 'pro taper'
745
+ ];
746
+ /** 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(' ')));
747
+ /**
748
+ * The one-word makers long enough that a tag one letter off still names them
749
+ * (AGL-4092): Pixabay's "susuki" is a Suzuki. Never a common word one letter
750
+ * off a maker: "barley" is not a Harley, nor "rental" a Renthal.
751
+ */ const FUZZY_MAKERS = [
752
+ ...MAKER_STEMS
753
+ ].filter((maker)=>!maker.includes(' ') && maker.length >= 6);
754
+ /** Words one letter off a maker that are words of their own (AGL-4092). */ const MAKER_LOOKALIKES = words('barley parley harlem rental samsun ducat phone suzuka');
755
+ /** Whether two words are one edit apart: a letter changed, added, dropped, or two side by side swapped. */ function oneEditApart(a, b) {
756
+ if (a === b) return false;
757
+ if (Math.abs(a.length - b.length) > 1) return false;
758
+ if (a.length === b.length) {
759
+ const differ = [
760
+ ...a
761
+ ].flatMap((letter, index)=>letter === b[index] ? [] : [
762
+ index
763
+ ]);
764
+ if (differ.length === 1) return true;
765
+ const [first, second] = differ;
766
+ return differ.length === 2 && second === first + 1 && a[first] === b[second] && a[second] === b[first];
767
+ }
768
+ const [short, long] = a.length < b.length ? [
769
+ a,
770
+ b
771
+ ] : [
772
+ b,
773
+ a
774
+ ];
775
+ let index = 0;
776
+ while(index < short.length && short[index] === long[index])index += 1;
777
+ return short.slice(index) === long.slice(index + 1);
778
+ }
779
+ /** The maker a word names one letter off, else `null` ({@link FUZZY_MAKERS}). */ function fuzzyMaker(word) {
780
+ var _FUZZY_MAKERS_find;
781
+ if (word.length < 5 || MAKER_LOOKALIKES.has(word) || MAKER_STEMS.has(word)) return null;
782
+ return (_FUZZY_MAKERS_find = FUZZY_MAKERS.find((maker)=>oneEditApart(word, maker))) != null ? _FUZZY_MAKERS_find : null;
783
+ }
784
+ /**
785
+ * A phrase's words and each pair of them in turn, as stems (AGL-4065): "dirt
786
+ * bike" names the motocross world where "bike" alone names a bicycle's too.
787
+ */ function withPairs(list) {
788
+ const out = [
789
+ ...list
790
+ ];
791
+ for(let index = 0; index + 1 < list.length; index += 1)out.push(`${list[index]} ${list[index + 1]}`);
792
+ return out;
793
+ }
794
+ /** A text's stems and stem pairs, every tag or phrase read on its own. */ function termsOf(texts) {
795
+ return new Set(texts.flatMap((text)=>withPairs(stemList(text))));
796
+ }
797
+ /** A word or phrase as stems: "dirt bikes" is "dirt bike". */ const stemPhrase = (phrase)=>stemList(phrase).join(' ');
798
+ /**
799
+ * A tag that says what a photo suits, never what it shows (AGL-4065):
800
+ * "samsung wallpaper", "iphone wallpaper", "lock screen". A latte tagged so
801
+ * shows no phone.
802
+ */ const SUITS = /\b(?:wallpapers?|backgrounds?|screens?|screensavers?|lockscreen|4k|hd)\b/i;
803
+ /** 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) {
804
+ var _photo_tags, _photo_alt;
805
+ const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).filter((tag)=>!SUITS.test(tag));
806
+ // "O'Neal" is one name, never "neal" (AGL-4092).
807
+ return termsOf([
808
+ ...tags,
809
+ (_photo_alt = photo.alt) != null ? _photo_alt : '',
810
+ pageSlug(photo.pageUrl)
811
+ ].map((text)=>text.replace(/['’]/g, '')));
812
+ }
550
813
  /**
551
814
  * Hands are never a picture's subject (AGL-3660): "hand-poured", "handmade"
552
815
  * and "by hand" say how a thing was made, and a search for "hands" found
@@ -735,11 +998,216 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
735
998
  'cream jar',
736
999
  'moisturizer'
737
1000
  ]
1001
+ },
1002
+ {
1003
+ // A rider's gear is what they wear (AGL-4065): "Riding Gear Item" took a
1004
+ // gear wheel of a transmission, tagged "gear, mechanics, bikes".
1005
+ objects: [
1006
+ 'gear'
1007
+ ],
1008
+ within: [
1009
+ 'motocross',
1010
+ 'motorcycle',
1011
+ 'motorbike',
1012
+ 'dirtbike',
1013
+ 'dirt bike',
1014
+ 'enduro',
1015
+ 'atv'
1016
+ ],
1017
+ kin: [
1018
+ 'riding gear',
1019
+ 'motocross gear',
1020
+ 'protective gear',
1021
+ 'helmet',
1022
+ 'helmets',
1023
+ 'motocross helmet',
1024
+ 'motorcycle helmet',
1025
+ 'jersey',
1026
+ 'boots',
1027
+ 'goggles',
1028
+ 'gloves'
1029
+ ],
1030
+ queries: [
1031
+ 'motocross gear',
1032
+ 'motocross helmet'
1033
+ ]
1034
+ },
1035
+ {
1036
+ // A dirt bike's tire is knobby; a car's on a highway is not one (AGL-4065).
1037
+ objects: [
1038
+ 'tire'
1039
+ ],
1040
+ within: [
1041
+ 'motocross',
1042
+ 'motorcycle',
1043
+ 'motorbike',
1044
+ 'dirtbike',
1045
+ 'dirt bike',
1046
+ 'enduro',
1047
+ 'atv'
1048
+ ],
1049
+ kin: [
1050
+ 'tire',
1051
+ 'tires',
1052
+ 'knobby tire',
1053
+ 'motocross tire',
1054
+ 'dirt bike tire',
1055
+ 'motorcycle tire',
1056
+ 'bike tire',
1057
+ 'wheel'
1058
+ ],
1059
+ queries: [
1060
+ 'dirt bike tire',
1061
+ 'motocross tire'
1062
+ ]
1063
+ },
1064
+ {
1065
+ // A brake pad is a brake's (AGL-4092): "Brake Pad Set" found nothing by its
1066
+ // pad, then searched "part" alone and took a transmission chain.
1067
+ objects: [
1068
+ 'brake',
1069
+ 'pad',
1070
+ 'rotor',
1071
+ 'caliper'
1072
+ ],
1073
+ within: [
1074
+ 'motocross',
1075
+ 'motorcycle',
1076
+ 'motorbike',
1077
+ 'dirtbike',
1078
+ 'dirt bike',
1079
+ 'enduro',
1080
+ 'atv'
1081
+ ],
1082
+ kin: [
1083
+ 'brake',
1084
+ 'brakes',
1085
+ 'brake pad',
1086
+ 'brake pads',
1087
+ 'brake disc',
1088
+ 'brake caliper',
1089
+ 'disc brake',
1090
+ 'brake rotor'
1091
+ ],
1092
+ queries: [
1093
+ 'motorcycle brake disc',
1094
+ 'dirt bike brake'
1095
+ ]
1096
+ },
1097
+ {
1098
+ // An engine's air is its filter's (AGL-4092): "Engine and air" tiled a
1099
+ // collection with a bare foot tagged "body part".
1100
+ objects: [
1101
+ 'engine',
1102
+ 'filter',
1103
+ 'air',
1104
+ 'carburetor'
1105
+ ],
1106
+ within: [
1107
+ 'motocross',
1108
+ 'motorcycle',
1109
+ 'motorbike',
1110
+ 'dirtbike',
1111
+ 'dirt bike',
1112
+ 'enduro',
1113
+ 'atv'
1114
+ ],
1115
+ kin: [
1116
+ 'engine',
1117
+ 'air filter',
1118
+ 'motorcycle engine',
1119
+ 'dirt bike engine',
1120
+ 'carburetor'
1121
+ ],
1122
+ queries: [
1123
+ 'dirt bike engine',
1124
+ 'motorcycle engine'
1125
+ ]
1126
+ },
1127
+ {
1128
+ // A taco trailer is a food truck, never a tank truck at a rest stop (AGL-4092).
1129
+ objects: [
1130
+ 'trailer',
1131
+ 'truck'
1132
+ ],
1133
+ within: [
1134
+ 'taco',
1135
+ 'taqueria',
1136
+ 'burrito',
1137
+ 'mexican food',
1138
+ 'street food',
1139
+ 'food truck'
1140
+ ],
1141
+ kin: [
1142
+ 'food truck',
1143
+ 'food trailer',
1144
+ 'taco truck',
1145
+ 'taco trailer',
1146
+ 'street food truck'
1147
+ ],
1148
+ queries: [
1149
+ 'taco truck',
1150
+ 'food truck'
1151
+ ]
1152
+ },
1153
+ {
1154
+ // A taco trailer's chips are tortilla chips, never crisps or a cookie's (AGL-4092).
1155
+ objects: [
1156
+ 'chip'
1157
+ ],
1158
+ within: [
1159
+ 'taco',
1160
+ 'taqueria',
1161
+ 'burrito',
1162
+ 'mexican food',
1163
+ 'street food',
1164
+ 'food truck'
1165
+ ],
1166
+ kin: [
1167
+ 'tortilla chip',
1168
+ 'tortilla chips',
1169
+ 'chips and salsa',
1170
+ 'chips salsa',
1171
+ 'nachos',
1172
+ 'nacho'
1173
+ ],
1174
+ queries: [
1175
+ 'tortilla chips salsa',
1176
+ 'nachos'
1177
+ ]
1178
+ },
1179
+ {
1180
+ // A fountain drink is a soda, never a well's fountain (AGL-4092).
1181
+ objects: [
1182
+ 'drink',
1183
+ 'soda'
1184
+ ],
1185
+ within: [
1186
+ 'taco',
1187
+ 'taqueria',
1188
+ 'burrito',
1189
+ 'mexican food',
1190
+ 'street food',
1191
+ 'food truck'
1192
+ ],
1193
+ // Never "fountain drink" itself: a well's page address says it word for word.
1194
+ kin: [
1195
+ 'soda',
1196
+ 'soft drink',
1197
+ 'iced tea',
1198
+ 'agua fresca',
1199
+ 'lemonade',
1200
+ 'soda cup'
1201
+ ],
1202
+ queries: [
1203
+ 'soda cup',
1204
+ 'agua fresca'
1205
+ ]
738
1206
  }
739
1207
  ];
740
1208
  /** An object's kin where the site or the subject is of its world, else `null`. */ function objectKin(object, world) {
741
1209
  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;
1210
+ 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
1211
  }
744
1212
  /**
745
1213
  * The kin of a few common crafts: the words a photo of that world is
@@ -1389,6 +1857,388 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1389
1857
  'analog'
1390
1858
  ]
1391
1859
  }
1860
+ },
1861
+ // The six trades of the 2026-10-25 booth kit (AGL-4065), none of which the
1862
+ // table listed: a landscaper and a handyman widened to an office, three
1863
+ // motocross businesses took race cars, cyclists, horses and scooters, and a
1864
+ // taco trailer that also takes catering requests was read as a caterer.
1865
+ {
1866
+ // A landscaper's photos are lawns, beds and gardens at work; a zen garden
1867
+ // of raked sand is not one (Bluebonnet's irrigation section).
1868
+ match: [
1869
+ 'landscaping',
1870
+ 'landscaper',
1871
+ 'landscapers',
1872
+ 'lawn',
1873
+ 'lawns',
1874
+ 'xeriscape',
1875
+ 'xeriscaping',
1876
+ 'sod',
1877
+ 'irrigation',
1878
+ 'hardscape',
1879
+ 'hardscaping',
1880
+ 'gardening',
1881
+ 'gardener',
1882
+ 'gardeners'
1883
+ ],
1884
+ kin: [
1885
+ 'landscaping',
1886
+ 'landscape',
1887
+ 'landscaper',
1888
+ 'lawn',
1889
+ 'grass',
1890
+ 'garden',
1891
+ 'yard',
1892
+ 'mower',
1893
+ 'mowing',
1894
+ 'lawn mower',
1895
+ 'turf',
1896
+ 'hedge',
1897
+ 'shrub',
1898
+ 'mulch',
1899
+ 'sprinkler',
1900
+ 'planting',
1901
+ 'flowerbed',
1902
+ 'flower bed',
1903
+ 'backyard',
1904
+ 'gardening',
1905
+ 'gardener',
1906
+ 'xeriscape',
1907
+ 'sod',
1908
+ 'irrigation'
1909
+ ],
1910
+ broad: [
1911
+ 'lawn care',
1912
+ 'landscaping garden',
1913
+ 'garden landscaping'
1914
+ ],
1915
+ making: [
1916
+ 'landscaper mowing lawn',
1917
+ 'gardener planting'
1918
+ ],
1919
+ shunLead: [
1920
+ 'zen',
1921
+ 'sand',
1922
+ 'office',
1923
+ 'business',
1924
+ 'desk',
1925
+ 'laptop',
1926
+ 'meeting',
1927
+ 'golf',
1928
+ 'stadium'
1929
+ ]
1930
+ },
1931
+ {
1932
+ // A handyman's photos are tools and repairs; never an architect's house
1933
+ // with a family before it, nor a job application (Two Oaks).
1934
+ match: [
1935
+ 'handyman',
1936
+ 'handymen',
1937
+ 'repairman',
1938
+ 'drywall',
1939
+ 'remodel',
1940
+ 'remodeling',
1941
+ 'remodeler',
1942
+ 'renovation',
1943
+ 'renovations',
1944
+ 'home repair'
1945
+ ],
1946
+ kin: [
1947
+ 'handyman',
1948
+ 'tools',
1949
+ 'tool',
1950
+ 'tool belt',
1951
+ 'toolbox',
1952
+ 'drill',
1953
+ 'hammer',
1954
+ 'screwdriver',
1955
+ 'wrench',
1956
+ 'ladder',
1957
+ 'workshop',
1958
+ 'repair',
1959
+ 'home repair',
1960
+ 'carpenter',
1961
+ 'carpentry',
1962
+ 'renovation',
1963
+ 'remodel',
1964
+ 'deck',
1965
+ 'fence',
1966
+ 'door',
1967
+ 'wall',
1968
+ 'plaster',
1969
+ 'drywall',
1970
+ 'construction'
1971
+ ],
1972
+ broad: [
1973
+ 'handyman tools',
1974
+ 'home repair',
1975
+ 'carpenter workshop'
1976
+ ],
1977
+ making: [
1978
+ 'handyman at work',
1979
+ 'handyman drilling'
1980
+ ],
1981
+ shunLead: [
1982
+ 'architecture',
1983
+ 'family',
1984
+ 'office',
1985
+ 'application',
1986
+ 'job',
1987
+ 'business',
1988
+ 'meeting',
1989
+ 'desk',
1990
+ 'laptop'
1991
+ ]
1992
+ },
1993
+ {
1994
+ // Motocross and the motorcycle world: a dirt bike, never a bicycle, a
1995
+ // race car, a horse or a scooter (Rut Runners, Holeshot, Berm & Bolt).
1996
+ match: [
1997
+ 'motocross',
1998
+ 'motorcycle',
1999
+ 'motorcycles',
2000
+ 'motorbike',
2001
+ 'motorbikes',
2002
+ 'dirtbike',
2003
+ 'dirtbikes',
2004
+ 'dirt bike',
2005
+ 'enduro',
2006
+ 'supercross',
2007
+ 'moto',
2008
+ 'mx',
2009
+ 'atv',
2010
+ 'quad bike',
2011
+ 'powersports'
2012
+ ],
2013
+ kin: [
2014
+ 'motocross',
2015
+ 'dirt bike',
2016
+ 'dirtbike',
2017
+ 'enduro',
2018
+ 'supercross',
2019
+ 'motorbike',
2020
+ 'motorcycle',
2021
+ 'mx',
2022
+ 'atv',
2023
+ 'quad bike',
2024
+ 'motocross track',
2025
+ 'motocross rider',
2026
+ 'dirt track'
2027
+ ],
2028
+ near: [
2029
+ 'rider',
2030
+ 'riders',
2031
+ 'helmet',
2032
+ 'jump',
2033
+ 'track',
2034
+ 'mud',
2035
+ 'dust',
2036
+ 'berm',
2037
+ 'racing',
2038
+ 'race',
2039
+ 'racer'
2040
+ ],
2041
+ broad: [
2042
+ 'motocross',
2043
+ 'dirt bike',
2044
+ 'motocross race',
2045
+ 'motocross rider'
2046
+ ],
2047
+ making: [
2048
+ 'motorcycle mechanic',
2049
+ 'motorcycle workshop'
2050
+ ],
2051
+ rivals: {
2052
+ words: [
2053
+ 'bicycle',
2054
+ 'bicycles',
2055
+ 'cycling',
2056
+ 'cyclist',
2057
+ 'cyclists',
2058
+ 'cycle racing',
2059
+ 'bike race',
2060
+ 'road bike',
2061
+ 'mountain bike',
2062
+ 'mtb',
2063
+ 'downhill',
2064
+ 'car',
2065
+ 'cars',
2066
+ 'race car',
2067
+ 'formula',
2068
+ 'kart',
2069
+ 'karting',
2070
+ 'horse',
2071
+ 'horses',
2072
+ 'equestrian',
2073
+ 'scooter',
2074
+ 'moped',
2075
+ 'vespa',
2076
+ 'cruiser',
2077
+ 'chopper'
2078
+ ],
2079
+ core: [
2080
+ 'motocross',
2081
+ 'motorcycle',
2082
+ 'motorbike',
2083
+ 'dirt bike',
2084
+ 'dirtbike',
2085
+ 'supercross',
2086
+ 'atv',
2087
+ 'quad bike'
2088
+ ]
2089
+ },
2090
+ shunLead: [
2091
+ 'car',
2092
+ 'scooter',
2093
+ 'moped',
2094
+ 'cruiser',
2095
+ 'chopper',
2096
+ 'vespa',
2097
+ 'bicycle',
2098
+ 'cyclist',
2099
+ 'horse',
2100
+ 'highway',
2101
+ 'asphalt',
2102
+ 'traffic'
2103
+ ],
2104
+ // A motocross team's "Gate drop" took a man posing with a street bike, tagged "man, model, motorcycle" (AGL-4092).
2105
+ wider: {
2106
+ words: [
2107
+ 'motorcycle',
2108
+ 'motorcycles',
2109
+ 'motorbike',
2110
+ 'motorbikes',
2111
+ 'moto'
2112
+ ],
2113
+ shunLead: [
2114
+ 'model',
2115
+ 'urban',
2116
+ 'street',
2117
+ 'road bike',
2118
+ 'road racing',
2119
+ 'sportbike',
2120
+ 'superbike',
2121
+ 'studio',
2122
+ 'posing'
2123
+ ]
2124
+ }
2125
+ },
2126
+ {
2127
+ // A parts shop of the motocross world: its parts, tires and riding gear,
2128
+ // never a canoe, a delivery or an industrial lathe (Berm & Bolt).
2129
+ match: [
2130
+ 'parts',
2131
+ 'part',
2132
+ 'aftermarket',
2133
+ 'oem',
2134
+ 'spare parts'
2135
+ ],
2136
+ of: [
2137
+ 'motocross',
2138
+ 'motorcycle',
2139
+ 'motorbike',
2140
+ 'dirtbike',
2141
+ 'dirt bike',
2142
+ 'enduro',
2143
+ 'supercross',
2144
+ 'moto',
2145
+ 'atv',
2146
+ 'quad bike',
2147
+ 'powersports'
2148
+ ],
2149
+ kin: [
2150
+ 'parts',
2151
+ 'part',
2152
+ 'spare part',
2153
+ 'motorcycle parts',
2154
+ 'tire',
2155
+ 'tires',
2156
+ 'wheel',
2157
+ 'chain',
2158
+ 'sprocket',
2159
+ 'brake',
2160
+ 'exhaust',
2161
+ 'riding gear',
2162
+ 'helmet'
2163
+ ],
2164
+ broad: [
2165
+ 'dirt bike parts',
2166
+ 'motorcycle parts',
2167
+ 'motocross gear'
2168
+ ],
2169
+ making: [
2170
+ 'motorcycle mechanic',
2171
+ 'motorcycle workshop'
2172
+ ],
2173
+ shunLead: [
2174
+ 'canoe',
2175
+ 'river',
2176
+ 'delivery',
2177
+ 'food',
2178
+ 'trees',
2179
+ 'office',
2180
+ 'industry',
2181
+ 'industrial',
2182
+ 'factory'
2183
+ ]
2184
+ },
2185
+ {
2186
+ // A taco trailer's photos are tacos and the truck, never a hotel's buffet,
2187
+ // a chef in whites or raw potatoes (Pit Row Tacos).
2188
+ match: [
2189
+ 'taco',
2190
+ 'tacos',
2191
+ 'taqueria',
2192
+ 'taco truck',
2193
+ 'taco trailer',
2194
+ 'burrito',
2195
+ 'burritos'
2196
+ ],
2197
+ kin: [
2198
+ 'taco',
2199
+ 'tacos',
2200
+ 'tortilla',
2201
+ 'tortillas',
2202
+ 'salsa',
2203
+ 'mexican',
2204
+ 'mexican food',
2205
+ 'street food',
2206
+ 'food truck',
2207
+ 'taco truck',
2208
+ 'griddle',
2209
+ 'burrito',
2210
+ 'quesadilla',
2211
+ 'nachos',
2212
+ 'guacamole',
2213
+ 'carnitas',
2214
+ 'pastor',
2215
+ 'asada'
2216
+ ],
2217
+ broad: [
2218
+ 'tacos',
2219
+ 'street tacos',
2220
+ 'taco truck'
2221
+ ],
2222
+ making: [
2223
+ 'taco truck cooking',
2224
+ 'tacos on the griddle'
2225
+ ],
2226
+ out: [
2227
+ 'food truck crowd',
2228
+ 'street food customers',
2229
+ 'taco stand customers'
2230
+ ],
2231
+ shunLead: [
2232
+ 'buffet',
2233
+ 'hotel',
2234
+ 'fine dining',
2235
+ 'chef',
2236
+ 'indian',
2237
+ 'potato',
2238
+ 'banquet',
2239
+ 'cafeteria',
2240
+ 'wedding'
2241
+ ]
1392
2242
  }
1393
2243
  ];
1394
2244
  /** Lowercase words, letters only, without stopwords. */ function contentWords(text) {
@@ -1409,28 +2259,60 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1409
2259
  return text.toLowerCase().split(/[^a-zÀ-ɏ]+/).filter((word)=>word.length > 1).map(aiStockStem);
1410
2260
  }
1411
2261
  /** 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) {
2262
+ /** 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));
2263
+ /**
2264
+ * A business type's clauses, each with the word that opened it. Where the
2265
+ * business's own clause is a word or two joined by "and" to a noun phrase
2266
+ * that ends in a place of business ("Motorcycle and dirt bike repair shop",
2267
+ * "Dirt bike and ATV parts shop"), the two are one clause (AGL-4065): the
2268
+ * Holeshot start read its business as "motorcycle" and lost the dirt bikes,
2269
+ * Berm & Bolt's as "dirt bike" and its craft as "bike". "And" before a verb
2270
+ * or a place still ends the clause. A clause that opens with what a customer
2271
+ * may ask ("takes", "books") loses that verb.
2272
+ */ function businessClauses(businessType) {
1413
2273
  const text = businessType.toLowerCase().replace(DOING, ' | ');
1414
2274
  const clauses = [];
1415
2275
  const breaker = new RegExp(`${BUSINESS_BREAK.source}|\\|`, 'g');
1416
2276
  let at = 0;
1417
2277
  let opener = '';
2278
+ const push = (raw)=>{
2279
+ var _found_;
2280
+ const found = contentWords(raw);
2281
+ // Never the type's first words: "Books and coffee" sells books.
2282
+ const asked = opener !== '' && found.length > 1 && TRANSACTION_VERBS.has((_found_ = found[0]) != null ? _found_ : '');
2283
+ clauses.push(_extends({
2284
+ opener,
2285
+ words: asked ? found.slice(1) : found
2286
+ }, asked ? {
2287
+ asked: true
2288
+ } : {}));
2289
+ };
1418
2290
  for (const match of text.matchAll(breaker)){
1419
2291
  var _match_index;
1420
- clauses.push({
1421
- opener,
1422
- words: contentWords(text.slice(at, match.index))
1423
- });
2292
+ push(text.slice(at, match.index));
1424
2293
  opener = match[0].trim();
1425
2294
  at = ((_match_index = match.index) != null ? _match_index : 0) + match[0].length;
1426
2295
  }
1427
- clauses.push({
1428
- opener,
1429
- words: contentWords(text.slice(at))
1430
- });
2296
+ push(text.slice(at));
2297
+ const own = clauses.findIndex((clause)=>businessOf(clause.words).length > 0);
2298
+ const first = clauses[own];
2299
+ const next = clauses[own + 1];
2300
+ if (first && next && (next.opener === 'and' || next.opener === '&')) {
2301
+ var _right_;
2302
+ const left = businessOf(first.words);
2303
+ const right = businessOf(next.words);
2304
+ const head = (_right_ = right[right.length - 1]) != null ? _right_ : '';
2305
+ if (left.length <= 2 && left.every((word)=>!VENUES.has(word) && !ROLES.has(word)) && right.length >= 2 && VENUES.has(head)) clauses.splice(own, 2, {
2306
+ opener: first.opener,
2307
+ words: [
2308
+ ...first.words,
2309
+ ...next.words
2310
+ ],
2311
+ joined: next.words
2312
+ });
2313
+ }
1431
2314
  return clauses;
1432
2315
  }
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
2316
  /**
1435
2317
  * The kind of business in a few words: the first clause of the business
1436
2318
  * type that says more than what kind of site it is, before a place, an
@@ -1438,9 +2320,13 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1438
2320
  * of an English noun phrase is at its end). "ceramic artist" from "a
1439
2321
  * portfolio for a ceramic artist who makes stoneware bowls"; "candle shop"
1440
2322
  * from "a small-batch candle shop selling hand-poured soy candles online".
2323
+ * A business named by two things joined by "and" is its second noun phrase
2324
+ * whole: "dirt bike repair shop" of "Motorcycle and dirt bike repair shop"
2325
+ * (AGL-4065).
1441
2326
  */ export function aiStockBusinessWords(businessType) {
1442
- for (const { words: clause } of businessClauses(businessType)){
2327
+ for (const { words: clause, joined } of businessClauses(businessType)){
1443
2328
  const named = businessOf(clause);
2329
+ if (joined) return businessOf(joined).slice(-4).join(' ');
1444
2330
  if (named.length) return named.slice(-3).join(' ');
1445
2331
  }
1446
2332
  return '';
@@ -1455,16 +2341,36 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1455
2341
  const craft = named[named.length - 1];
1456
2342
  return craft ? aiStockStem(craft) : '';
1457
2343
  }
1458
- /** The site's terms, read from its business type. */ export function aiStockSiteTerms(businessType) {
2344
+ /** 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);
2345
+ /**
2346
+ * The site's terms, read from its business type.
2347
+ *
2348
+ * Two worlds are read (AGL-4065). Its DOMAIN is everything its words name
2349
+ * of its world, what a picture of a thing is held to. Its LEAD is the
2350
+ * business's own world: the listed crafts its own clause names (and the
2351
+ * craft's), their names and kin, and the clause's own words where no craft
2352
+ * covers them, never a word that names a world only beside another ("race",
2353
+ * "team") nor what a customer is asked to do. A hero's first tag and a broad
2354
+ * search's hit name the lead: the Rut Runners heroes, held to every word of
2355
+ * "Amateur motocross race team: rider profiles, race schedule…", took race
2356
+ * cars because "race car" named "race". Of the crafts its words list, the one
2357
+ * named first leads (a taco trailer that "takes catering requests" is a taco
2358
+ * trailer), and a craft named only in what a customer may ask for gives the
2359
+ * site no searches of its own.
2360
+ */ export function aiStockSiteTerms(businessType) {
1459
2361
  let business = aiStockBusinessWords(businessType);
1460
2362
  let craft = aiStockCraftWords(business);
1461
2363
  const domain = new Set();
1462
2364
  /** The things it makes, by their head nouns. */ const made = new Set();
2365
+ /** 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();
2366
+ const asked = new Set();
2367
+ /** The business's own clause, as stems and stem pairs: what its lead is read from. */ let own = [];
2368
+ let ownSeen = false;
1463
2369
  /** The last word of the first clause that names the world: "soap" of "for handmade cold-process soap". */ let firstWorld = '';
1464
2370
  let named = false;
1465
2371
  let making = false;
1466
2372
  let place = false;
1467
- for (const { opener, words: clause } of businessClauses(businessType)){
2373
+ for (const { opener, words: clause, asked: isAsked } of businessClauses(businessType)){
1468
2374
  if (opener === '|') making = true;
1469
2375
  // Where it is, and who it is for, are not its world — once something has named it.
1470
2376
  if (named && PLACE_OR_AUDIENCE.test(opener)) {
@@ -1478,6 +2384,10 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1478
2384
  if (place && opener === ',') continue;
1479
2385
  place = false;
1480
2386
  const kept = businessOf(clause).filter((word)=>!VENUES.has(word) && !ROLES.has(word));
2387
+ if (!ownSeen && businessOf(clause).length) {
2388
+ ownSeen = true;
2389
+ own = kept.filter((word)=>!LEAD_GENERIC.has(word));
2390
+ }
1481
2391
  if (kept.length) named = true;
1482
2392
  // What it makes names its world by what the things are made of, never
1483
2393
  // the things: "stoneware" of "stoneware bowls", "soy" of "soy candles".
@@ -1485,32 +2395,55 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1485
2395
  // A set or a gift is never a world: "gift" of "gift sets" is not one.
1486
2396
  const world = (making ? kept.slice(0, -1) : kept).filter((word)=>!WRAPPERS.has(word));
1487
2397
  for (const word of world)for (const stem of stemList(word))domain.add(stem);
2398
+ for (const term of termsOf([
2399
+ kept.join(' ')
2400
+ ]))(isAsked ? asked : said).add(term);
1488
2401
  const last = world[world.length - 1];
1489
2402
  if (!firstWorld && last) firstWorld = aiStockStem(last);
1490
2403
  if (making) for (const word of kept.slice(-1))for (const stem of stemList(word))made.add(stem);
1491
2404
  }
1492
2405
  // A business named only by what it sells ("a shop selling pottery") is of that world.
1493
2406
  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));
2407
+ const typeStems = stemList(businessType);
2408
+ const typeTerms = withPairs(typeStems);
2409
+ /** Where a word or a two-word name first stands in the business type; the end where it stands nowhere. */ const positionOf = (term)=>{
2410
+ const [head, tail] = term.split(' ');
2411
+ const index = typeStems.findIndex((stem, at)=>stem === head && (tail === undefined || typeStems[at + 1] === tail));
2412
+ return index >= 0 ? index : Number.MAX_SAFE_INTEGER;
2413
+ };
2414
+ /** 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))));
2415
+ const names = (word)=>{
2416
+ const term = groupTerm(word);
2417
+ return term.includes(' ') ? said.has(term) || asked.has(term) : domain.has(term) || made.has(term) || term === craft;
2418
+ };
2419
+ const found = CRAFT_KIN.map((group, index)=>({
2420
+ group,
2421
+ index
2422
+ })).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);
2423
+ const groups = found.map(({ group })=>group);
2424
+ // A craft named only in what the business is asked for ("takes catering
2425
+ // requests") lends its kin and its refusals, never its searches (AGL-4065).
2426
+ const major = groups.filter((group)=>group.match.some((word)=>said.has(groupTerm(word)) || made.has(groupTerm(word))));
1495
2427
  // A business that is only a place ("an online shop for handmade
1496
2428
  // cold-process soap") takes its craft from what it is for: the Saltmarsh
1497
2429
  // Soap Co start (2026-10-10) read its craft as nothing, so its hero searched
1498
2430
  // "shop" (a florist's bouquet) and no product was held to soap.
1499
2431
  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));
2432
+ var _major_;
2433
+ const listed = (_major_ = major[0]) == null ? void 0 : _major_.match.map(groupTerm).find((stem)=>!stem.includes(' ') && (domain.has(stem) || made.has(stem)));
1502
2434
  craft = listed || firstWorld || [
1503
2435
  ...made
1504
2436
  ][0] || '';
1505
2437
  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;
2438
+ } else if (major[0] && !major.some((group)=>group.match.some((word)=>groupTerm(word) === craft))) {
2439
+ var _major__match_map_find;
1508
2440
  // A word the table does not list, beside a craft it does, says what kind
1509
2441
  // of that craft it is (AGL-3856): "wedding" of "a wedding and portrait
1510
2442
  // 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;
2443
+ 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
2444
  }
1513
2445
  if (craft) domain.add(craft);
2446
+ const craftGroup = major.find((group)=>group.match.some((word)=>groupTerm(word) === craft));
1514
2447
  const broad = [];
1515
2448
  const makingSearches = [];
1516
2449
  const rivals = [];
@@ -1520,25 +2453,96 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1520
2453
  const shun = new Set();
1521
2454
  const shunLead = new Set();
1522
2455
  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);
2456
+ var _group_shun, _group_shunLead, _group_near;
2457
+ for (const word of (_group_shun = group.shun) != null ? _group_shun : [])shun.add(groupTerm(word));
2458
+ for (const word of (_group_shunLead = group.shunLead) != null ? _group_shunLead : [])shunLead.add(groupTerm(word));
2459
+ for (const word of [
2460
+ ...group.kin,
2461
+ ...(_group_near = group.near) != null ? _group_near : []
2462
+ ])domain.add(groupTerm(word));
1530
2463
  if (group.rivals) rivals.push({
1531
- words: group.rivals.words.map(aiStockStem),
1532
- core: group.rivals.core.map(aiStockStem)
2464
+ words: group.rivals.words.map(groupTerm),
2465
+ core: group.rivals.core.map(groupTerm)
1533
2466
  });
1534
2467
  }
2468
+ for (const group of major){
2469
+ for (const word of group.match)core.add(groupTerm(word));
2470
+ broad.push(...group.broad);
2471
+ makingSearches.push(...group.making);
2472
+ }
2473
+ // A two-word name of a listed world names it whole: "dirt bike", never
2474
+ // "bike" (a bicycle's) where the type says it nowhere else (AGL-4065).
2475
+ for (const group of groups)for (const word of group.match){
2476
+ const term = groupTerm(word);
2477
+ if (!term.includes(' ') || !typeTerms.includes(term)) continue;
2478
+ domain.add(term);
2479
+ const [head, tail] = term.split(' ');
2480
+ const alone = (stem)=>typeStems.some((found, index)=>found === stem && !(found === head ? typeStems[index + 1] === tail : typeStems[index - 1] === head));
2481
+ for (const stem of [
2482
+ head,
2483
+ tail
2484
+ ])if (!alone(stem)) domain.delete(stem);
2485
+ }
2486
+ // What a customer is asked to do is never a world (AGL-4065).
2487
+ for (const stem of TRANSACTION_STEMS)domain.delete(stem);
2488
+ // The business's own world: the crafts its own clause names, and the craft's.
2489
+ const ownTerms = new Set(termsOf([
2490
+ own.join(' ')
2491
+ ]));
2492
+ const leading = major.filter((group)=>group === craftGroup || group.match.some((word)=>ownTerms.has(groupTerm(word))));
2493
+ const lead = new Set();
2494
+ for (const group of leading)for (const word of [
2495
+ ...group.match,
2496
+ ...group.kin
2497
+ ])lead.add(groupTerm(word));
2498
+ if (leading.length) {
2499
+ for (const word of own)for (const stem of stemList(word))if (![
2500
+ ...lead
2501
+ ].some((term)=>term === stem || term.includes(' ') && term.split(' ').includes(stem) && ownTerms.has(term))) lead.add(stem);
2502
+ }
2503
+ // A wider world's names lead only where the business's own words say them
2504
+ // (AGL-4092): a motocross team's world is off-road, never "motorcycle" alone.
2505
+ for (const group of leading){
2506
+ if (!group.wider) continue;
2507
+ const wider = group.wider.words.map(groupTerm);
2508
+ if (wider.some((term)=>ownTerms.has(term))) continue;
2509
+ for (const term of wider)lead.delete(term);
2510
+ for (const word of group.wider.shunLead)shunLead.add(groupTerm(word));
2511
+ }
2512
+ // A trade's world, named by the world it serves: "dirt bike" for a dirt bike parts shop.
2513
+ 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
2514
  if (!broad.length) broad.push(business, craft);
2515
+ // A part or gear names no world on its own: a dirt bike shop's part, never a canoe's (AGL-4065).
2516
+ const ofAWorld = (term)=>term.length > 1 && !term.split(' ').every((word)=>WORLD_OBJECTS.has(word));
2517
+ // A trade's things are held to the world it serves, never to its own word (AGL-4092).
2518
+ const tradeNames = new Set(((craftGroup == null ? void 0 : craftGroup.of) ? craftGroup.match : []).map(groupTerm));
2519
+ const held = (craftGroup == null ? void 0 : craftGroup.of) ? [
2520
+ ...core
2521
+ ].filter((term)=>ofAWorld(term) && !tradeNames.has(term)) : [];
2522
+ const out = [
2523
+ ...new Set(leading.flatMap((group)=>{
2524
+ var _group_out;
2525
+ return (_group_out = group.out) != null ? _group_out : [];
2526
+ }))
2527
+ ];
1536
2528
  return _extends({
1537
2529
  business,
1538
2530
  craft,
1539
2531
  domain: [
1540
2532
  ...domain
1541
2533
  ].filter((stem)=>stem.length > 1),
2534
+ lead: leading.length ? [
2535
+ ...lead
2536
+ ].filter(ofAWorld) : [
2537
+ ...domain
2538
+ ].filter((stem)=>stem.length > 1)
2539
+ }, served ? {
2540
+ leadWord: served
2541
+ } : {}, held.length ? {
2542
+ held
2543
+ } : {}, out.length ? {
2544
+ out
2545
+ } : {}, {
1542
2546
  broad: [
1543
2547
  ...new Set(broad.map((query)=>query.trim()).filter(Boolean))
1544
2548
  ],
@@ -1559,12 +2563,12 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1559
2563
  ]
1560
2564
  } : {}, {
1561
2565
  named: [
1562
- ...new Set(stemList(businessType))
2566
+ ...new Set(typeTerms)
1563
2567
  ],
1564
2568
  rivals,
1565
2569
  core: [
1566
2570
  ...core
1567
- ],
2571
+ ].filter((term)=>!(craftGroup == null ? void 0 : craftGroup.of) || ofAWorld(term)),
1568
2572
  shun: [
1569
2573
  ...shun
1570
2574
  ],
@@ -1578,20 +2582,45 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1578
2582
  * the beta.266 Mexicano portfolio's gallery was "Project one" to "Project
1579
2583
  * four", and searched "project" (students at a whiteboard), "project four"
1580
2584
  * (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');
2585
+ */ 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".
2586
+ 'upcoming past next previous future');
1582
2587
  /** 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));
2588
+ return list.filter((word)=>!FILLER.has(word) && !PLACEHOLDER_SUBJECTS.has(word) && !GENERIC_OBJECTS.has(word) && !SENTENCE_WORDS.has(word));
1584
2589
  }
1585
2590
  /**
1586
2591
  * A picture's subject in a few words (AGL-3660): the noun phrase its alt
1587
2592
  * text opens with — its first clause holding more than filler, cut where a
1588
2593
  * preposition starts the setting, filler dropped, its last three words (the
1589
- * head noun is the last) — else its section's name.
2594
+ * head noun is the last) — else its section's name. A word or two joined by
2595
+ * "and" to a longer noun phrase only modify its head: the subject of "Potato
2596
+ * and egg breakfast taco" is "egg breakfast taco", never "potato" (AGL-4065).
1590
2597
  */ export function aiStockSubjectWords(alt, sectionName) {
1591
2598
  // "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(' ');
2599
+ const text = withoutHands(alt);
2600
+ const breaker = new RegExp(CLAUSE_BREAK.source, 'g');
2601
+ const clauses = [];
2602
+ let at = 0;
2603
+ for (const match of text.matchAll(breaker)){
2604
+ var _match_index;
2605
+ clauses.push({
2606
+ words: subjectOf(contentWords(text.slice(at, match.index))),
2607
+ then: match[0].trim()
2608
+ });
2609
+ at = ((_match_index = match.index) != null ? _match_index : 0) + match[0].length;
2610
+ }
2611
+ clauses.push({
2612
+ words: subjectOf(contentWords(text.slice(at))),
2613
+ then: ''
2614
+ });
2615
+ for (const [index, { words: found, then }] of clauses.entries()){
2616
+ var _ref;
2617
+ var _clauses_;
2618
+ if (!found.length) continue;
2619
+ // Never where the second is one thing in a wrapper: "Soap and Lotion Gift Set" is a gift of both.
2620
+ const next = (_ref = (_clauses_ = clauses[index + 1]) == null ? void 0 : _clauses_.words) != null ? _ref : [];
2621
+ const things = next.filter((word)=>!WRAPPERS.has(word));
2622
+ if ((then === 'and' || then === '&') && found.length <= 2 && things.length >= 2) return next.slice(-3).join(' ');
2623
+ return found.slice(-3).join(' ');
1595
2624
  }
1596
2625
  return subjectOf(contentWords(withoutHands(sectionName))).slice(0, 2).join(' ');
1597
2626
  }
@@ -1599,10 +2628,12 @@ const words = (list)=>new Set(list.trim().split(/\s+/));
1599
2628
  * The thing a subject shows, one singular word: its last word that is not
1600
2629
  * a set or a wrapper of it — "bowl" from "nesting bowl set", "candle" from
1601
2630
  * "candle gift set", "vase" from "bud vase". Its last word when every word
1602
- * is one ("gift set").
2631
+ * is one ("gift set"). A feeling, a time or a way of being somewhere is
2632
+ * never one ({@link NON_THINGS}, AGL-4092): "Come hungry" and "Around town"
2633
+ * show no thing, and "Race day" is of the race.
1603
2634
  */ export function aiStockObjectWord(subject) {
1604
2635
  var _ref, _reverse_find;
1605
- const list = subject.split(/\s+/).filter(Boolean);
2636
+ const list = subject.split(/\s+/).filter((word)=>word && !NON_THINGS.has(word));
1606
2637
  const object = (_ref = (_reverse_find = [
1607
2638
  ...list
1608
2639
  ].reverse().find((word)=>!WRAPPERS.has(word))) != null ? _reverse_find : list[list.length - 1]) != null ? _ref : '';
@@ -2378,7 +3409,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2378
3409
  * product names it. A size never names the product: "Bath Gift Set Starter
2379
3410
  * Size" is a bath gift set.
2380
3411
  */ export function aiStockProductSearches(slot, craft, domain = [], fallback = {}) {
2381
- var _ref, _subjects_, _fallback_core, _ref1, _fallback_broad, _fallback_broad1;
3412
+ var _ref, _subjects_, _fallback_held, _fallback_core, _fallback_leadWord, _ref1, _fallback_broad, _fallback_broad1;
2382
3413
  var _slot_product;
2383
3414
  const orientation = aiStockOrientation(slot.aspect);
2384
3415
  const size = orientation === 'vertical' ? {
@@ -2404,23 +3435,31 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2404
3435
  // A product of the shop's world beside its category is held to either:
2405
3436
  // a soap shop's body lotion names "lotion", never "soap".
2406
3437
  const ownWorld = stemList(own).filter((stem)=>world.has(stem) && !WRAPPERS.has(stem) && !craftStems.includes(stem));
2407
- const category = craft && ownWorld.length ? [
3438
+ // A trade's product is held to the world it serves, never to its craft
3439
+ // (AGL-4092): a parts shop's "part" named a transmission's "mechanical part".
3440
+ const served = [
3441
+ ...new Set(((_fallback_held = fallback.held) != null ? _fallback_held : []).map(stemPhrase).filter(Boolean))
3442
+ ];
3443
+ const trade = served.length > 0;
3444
+ const category = trade ? served : craft && ownWorld.length ? [
2408
3445
  ...craftStems,
2409
3446
  ...ownWorld
2410
3447
  ] : [];
2411
- const core = [
3448
+ const core = trade ? served : [
2412
3449
  ...new Set([
2413
3450
  ...craftStems,
2414
3451
  ...(_fallback_core = fallback.core) != null ? _fallback_core : []
2415
3452
  ])
2416
3453
  ];
3454
+ // A trade's product is named by the world it serves: "dirt bike skid plate", never "part skid plate".
3455
+ const prefix = trade ? (_fallback_leadWord = fallback.leadWord) != null ? _fallback_leadWord : '' : craft;
2417
3456
  const shun = ((_ref1 = kin == null ? void 0 : kin.shun) != null ? _ref1 : []).map(aiStockStem);
2418
3457
  const queries = [];
2419
3458
  for (const subject of subjects){
2420
3459
  var _ref2;
2421
3460
  const stems = stemList(subject);
2422
3461
  const qualifier = (_ref2 = kin == null ? void 0 : kin.qualifier) != null ? _ref2 : '';
2423
- const query = kin ? !qualifier || stems.includes(aiStockStem(qualifier)) ? subject : kin.after ? `${subject} ${qualifier}` : `${qualifier} ${subject}` : craft && !stems.some((stem)=>world.has(stem)) ? `${craft} ${subject}` : subject;
3462
+ const query = kin ? !qualifier || stems.includes(aiStockStem(qualifier)) ? subject : kin.after ? `${subject} ${qualifier}` : `${qualifier} ${subject}` : prefix && !stems.some((stem)=>world.has(stem)) ? `${prefix} ${subject}` : subject;
2424
3463
  queries.push({
2425
3464
  query,
2426
3465
  subject,
@@ -2444,7 +3483,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2444
3483
  object,
2445
3484
  lead: true
2446
3485
  });
2447
- else if (craft) queries.push({
3486
+ else if (craft && !trade) queries.push({
2448
3487
  query: craft,
2449
3488
  subject: own || craft,
2450
3489
  object: craft,
@@ -2507,6 +3546,8 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2507
3546
  tier: entry.tier
2508
3547
  } : {}, shun.length ? {
2509
3548
  shun
3549
+ } : {}, trade ? {
3550
+ served
2510
3551
  } : {});
2511
3552
  if (entry.broad) {
2512
3553
  const heldTo = [
@@ -2523,9 +3564,11 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2523
3564
  kin: kinPhrases
2524
3565
  } : {}, heldTo.length ? {
2525
3566
  category: heldTo
2526
- } : {}) : core.length ? {
3567
+ } : {}) : core.length ? _extends({
2527
3568
  category: core
2528
- } : {}));
3569
+ }, trade ? {
3570
+ leadOwn: true
3571
+ } : {}) : {}));
2529
3572
  continue;
2530
3573
  }
2531
3574
  searches.push(_extends({}, common, entry.lead ? {
@@ -2536,7 +3579,9 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2536
3579
  object: entry.object
2537
3580
  } : {}, kinPhrases.length && entry.object === object ? {
2538
3581
  kin: kinPhrases
2539
- } : {}, craft ? {
3582
+ } : {}, trade ? {
3583
+ strict: true
3584
+ } : craft ? {
2540
3585
  requires: craft
2541
3586
  } : {}, category.length ? {
2542
3587
  category
@@ -2550,8 +3595,8 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2550
3595
  * asks first for the role its words or its list's name (`sectionName`) say,
2551
3596
  * in the site's world: "teacher classroom" for a "Math instructor" (AGL-3856).
2552
3597
  */ 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;
2554
- var _aiStockPeopleRole, _slot_record, _slot_record1;
3598
+ var _terms_core, _terms_leadWord, _terms_making, _ref, _ref1, _terms_setting, _terms_setting1, _ref2, _terms_maker, _terms_maker1, _ref3;
3599
+ var _terms_held, _aiStockPeopleRole, _slot_record, _terms_out, _slot_record1;
2555
3600
  const orientation = aiStockOrientation(slot.aspect);
2556
3601
  const size = slot.role === 'hero' ? {
2557
3602
  minWidth: 1600
@@ -2596,16 +3641,36 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2596
3641
  // A hero of a craft names the craft itself: the Saltmarsh Soap Co hero
2597
3642
  // (2026-10-10) was a florist's bouquet tagged "natural" (AGL-3660).
2598
3643
  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;
3644
+ // The world a trade serves qualifies its pictures where the craft is the trade: "dirt bike", never "part" (AGL-4065).
3645
+ const leadWord = (_terms_leadWord = terms.leadWord) != null ? _terms_leadWord : terms.craft;
3646
+ const leadStems = stemList(leadWord);
3647
+ const qualify = (phrase)=>leadWord && !namesWorld && !leadStems.every((stem)=>stemList(phrase).includes(stem)) ? `${leadWord} ${phrase}` : phrase;
3648
+ // A part, gear, a machine, or one word of a two-word world ("bike" of "dirt
3649
+ // bike"), is never searched alone (AGL-4065): searched with the world, and
3650
+ // its photo is OF the thing and names the world. "Parts for the ranch"
3651
+ // searched "parts" and took a canoe tagged "part".
3652
+ 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);
3653
+ /** 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
3654
  // A collection tile is product-like: held to the shop's category and its
2601
3655
  // own object among the first tags, and searched by its object's kin, never
2602
3656
  // the category's world alone (AGL-3660).
2603
3657
  const collection = part === 'collection' && Boolean(terms.craft) && !work;
2604
- const broad = kin && collection || work ? [] : terms.broad.filter(shown).map((query)=>({
3658
+ // A picture's broad searches find a photo OF the business's own world,
3659
+ // never one naming it in a word of a tag (AGL-4092); a hero's first tag
3660
+ // already names it.
3661
+ const broad = kin && collection || work ? [] : terms.broad.filter(shown).map((query)=>_extends({
2605
3662
  query,
2606
3663
  broad: true,
2607
3664
  category: collection
3665
+ }, slot.role === 'hero' ? {} : {
3666
+ own: true
2608
3667
  }));
3668
+ // A trade's things are held to the world it serves, never to its craft (AGL-4092).
3669
+ const heldTo = ((_terms_held = terms.held) == null ? void 0 : _terms_held.length) ? terms.held : null;
3670
+ // A caption that names no thing (AGL-4092): "Come hungry" is a picture of
3671
+ // the site's own world, and "Around town" of its customers out where it is.
3672
+ const thingless = Boolean(subject) && !object && !work;
3673
+ const out = thingless && slot.role !== 'hero' && PLACE_PHRASES.test(slot.alt);
2609
3674
  // The maker at work, held to the craft: never "hands" (AGL-3660).
2610
3675
  const makers = (work ? [] : (_terms_making = terms.making) != null ? _terms_making : []).filter(shown).map((query)=>({
2611
3676
  query,
@@ -2627,21 +3692,61 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2627
3692
  }
2628
3693
  // The work's own subject is never qualified by the craft: "getting ready", never "photographer getting ready".
2629
3694
  const own = work ? (phrase)=>phrase : qualify;
2630
- const thing = (subject ? [
3695
+ // A record's cover is OF what it names, searched in the site's world first
3696
+ // (AGL-4065): "handyman drywall" before "drywall", which took an architect's
3697
+ // house with a family before its studs.
3698
+ const record = Boolean(slot.record) && !work && !ofTheWorld && Boolean(leadWord);
3699
+ const thing = (!subject ? [] : thingless ? out ? (((_terms_out = terms.out) == null ? void 0 : _terms_out.length) ? terms.out : [
3700
+ `${leadWord} customers`.trim()
3701
+ ]).map((query)=>({
3702
+ query,
3703
+ out: true
3704
+ })) : [
2631
3705
  {
2632
- query: own(subject),
2633
- held: true
3706
+ query: `${leadWord} ${subject}`.trim(),
3707
+ own: true
3708
+ }
3709
+ ] : ofTheWorld ? [
3710
+ {
3711
+ query: `${leadWord} ${subjectStems.some((stem)=>!leadStems.includes(stem)) ? subject.split(/\s+/).filter((word)=>!leadStems.includes(aiStockStem(word))).join(' ') : ''}`.trim(),
3712
+ held: true,
3713
+ leads: true,
3714
+ ofWorld: true
2634
3715
  },
2635
3716
  ...kin ? kin.queries.map((query)=>({
2636
3717
  query,
2637
- held: true
2638
- })) : [
3718
+ held: true,
3719
+ leads: true,
3720
+ ofWorld: true
3721
+ })) : []
3722
+ ] : [
3723
+ ...record && !leadStems.every((stem)=>subjectStems.includes(stem)) ? [
2639
3724
  {
3725
+ query: `${leadWord} ${subject}`,
3726
+ held: true,
3727
+ leads: true
3728
+ }
3729
+ ] : [],
3730
+ _extends({
3731
+ query: own(subject),
3732
+ held: true
3733
+ }, record ? {
3734
+ leads: true
3735
+ } : {}),
3736
+ ...kin ? kin.queries.map((query)=>_extends({
3737
+ query,
3738
+ held: true
3739
+ }, record ? {
3740
+ leads: true
3741
+ } : {})) : [
3742
+ _extends({
2640
3743
  query: own(object),
2641
3744
  held: true
2642
- }
3745
+ }, record ? {
3746
+ leads: true
3747
+ } : {})
2643
3748
  ]
2644
- ] : []).filter((query)=>said(query.query));
3749
+ ]).filter((query)=>said(query.query));
2645
3750
  // Never the maker, the camera, an animal or a toy, unless the record names it.
2646
3751
  const recordNamed = work ? new Set(stemList(`${slot.alt} ${(_ref2 = (_slot_record1 = slot.record) == null ? void 0 : _slot_record1.category) != null ? _ref2 : ''}`)) : null;
2647
3752
  const refuseLead = recordNamed ? [
@@ -2795,20 +3900,36 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2795
3900
  broad: true
2796
3901
  } : {}, held && kinPhrases.length ? {
2797
3902
  kin: kinPhrases
2798
- } : {}, held && collection ? {
3903
+ } : {}, held && collection || raw.leads ? {
2799
3904
  lead: true
2800
- } : {}, terms.craft && (raw.category || raw.held && collection) ? _extends({
3905
+ } : {}, terms.craft && !raw.ofWorld && (raw.category || raw.held && collection) ? heldTo ? {
3906
+ category: heldTo,
3907
+ served: heldTo
3908
+ } : _extends({
2801
3909
  requires: terms.craft
2802
3910
  }, ownWorld.length ? {
2803
3911
  category: [
2804
3912
  terms.craft,
2805
3913
  ...ownWorld
2806
3914
  ]
2807
- } : {}) : {}, slot.role === 'hero' && core.length ? {
3915
+ } : {}) : {}, raw.ofWorld && worldNames.length ? {
3916
+ category: worldNames
3917
+ } : {}, slot.role === 'hero' && core.length ? {
2808
3918
  category: core
2809
3919
  } : {}, slot.role === 'hero' ? {
2810
3920
  first: true
2811
- } : {}, refuseLead.length ? {
3921
+ } : {}, raw.own ? {
3922
+ leadOwn: true
3923
+ } : {}, raw.out ? {
3924
+ broad: true,
3925
+ needs: PLACE_NEEDS.map(stemPhrase),
3926
+ refuseLead: [
3927
+ ...new Set([
3928
+ ...refuseLead,
3929
+ ...FOREIGN_STREET.map(stemPhrase)
3930
+ ])
3931
+ ]
3932
+ } : refuseLead.length ? {
2812
3933
  refuseLead
2813
3934
  } : {}, work ? {
2814
3935
  worldless: true
@@ -2849,15 +3970,17 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2849
3970
  var _photo_tags, _photo_alt, _photo_alt1, _tags_, _photo_alt2;
2850
3971
  const slug = pageSlug(photo.pageUrl);
2851
3972
  const tags = ((_photo_tags = photo.tags) != null ? _photo_tags : []).map((tag)=>tag.toLowerCase().trim()).filter(Boolean);
2852
- const lead = new Set(stemList([
3973
+ // Each tag's words and pairs of them, read tag by tag (AGL-4065): "race car"
3974
+ // and "dirt bike" are phrases a world names; the page address is read whole.
3975
+ const lead = termsOf([
2853
3976
  ...tags.slice(0, 3),
2854
3977
  slug
2855
- ].join(' ')));
2856
- const all = new Set(stemList([
3978
+ ]);
3979
+ const all = termsOf([
2857
3980
  ...tags,
2858
3981
  (_photo_alt = photo.alt) != null ? _photo_alt : '',
2859
3982
  slug
2860
- ].join(' ')));
3983
+ ]);
2861
3984
  const phrases = [
2862
3985
  ...tags.map((tag)=>stemList(tag).join(' ')),
2863
3986
  stemList((_photo_alt1 = photo.alt) != null ? _photo_alt1 : '').join(' '),
@@ -2885,12 +4008,61 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2885
4008
  };
2886
4009
  }
2887
4010
  /** Whether a phrase is among a hit's phrases, word for word. */ const among = (phrases, phrase)=>phrases.some((found)=>` ${found} `.includes(` ${phrase} `));
4011
+ /**
4012
+ * Whether one of a hit's first three tags is OF a world (AGL-4092): it names
4013
+ * the world, and its last word, or its last two, is a word of the world or of
4014
+ * its domain. "Street tacos" and "dirt bike tire" are; "taco bowl" is a bowl,
4015
+ * "body part" a part. Read from the tags alone: the page address repeats them
4016
+ * run together, and a run of words has no head.
4017
+ */ function leadsWithWorld(tags, world, domain) {
4018
+ return tags.slice(0, 3).some((tag)=>{
4019
+ var _stems_;
4020
+ const stems = stemList(tag);
4021
+ if (!withPairs(stems).some((term)=>world.has(term))) return false;
4022
+ return [
4023
+ (_stems_ = stems[stems.length - 1]) != null ? _stems_ : '',
4024
+ stems.slice(-2).join(' ')
4025
+ ].some((term)=>Boolean(term) && (world.has(term) || domain.has(term)));
4026
+ });
4027
+ }
4028
+ /**
4029
+ * What a picture of a business's world is never OF unless its words name it
4030
+ * (AGL-4092): an animal or a toy (as a portfolio's record never is,
4031
+ * {@link RECORD_ODD_LEADS}), or a bare body part. Each group is let in whole
4032
+ * by any of its words in `said` — the slot's words, its section's, the
4033
+ * brief's — so a dog groomer's pictures show dogs and cats alike. As stems;
4034
+ * the body's left out where `body` is false (a portrait's face).
4035
+ */ export function aiStockOddLeads(said, options = {}) {
4036
+ const named = new Set(said.flatMap((text)=>stemList(text)));
4037
+ const groups = [
4038
+ 'animal animals cat cats kitten kittens dog dogs puppy puppies pet pets bird birds frog frogs duck ducks horse horses ' + 'cow cows rabbit rabbits bunny',
4039
+ 'toy toys figurine figurines lego doll dolls teddy puppet miniature',
4040
+ ...options.body === false ? [] : [
4041
+ `${[
4042
+ ...BODY_PARTS
4043
+ ].join(' ')} toe toes barefoot`
4044
+ ]
4045
+ ].map((group)=>[
4046
+ ...new Set(stemList(group))
4047
+ ]);
4048
+ return groups.filter((group)=>!group.some((stem)=>named.has(stem))).flat();
4049
+ }
2888
4050
  /**
2889
4051
  * Whether a hit carries a topic its site never named ({@link SENSITIVE_TOPICS}):
2890
4052
  * a cross, a grave, a flag, a gun, a glass of wine, a child, a mannequin.
2891
4053
  */ export function aiStockSensitive(photo, named = []) {
2892
4054
  const { words: found } = hitWords(photo);
2893
4055
  const site = new Set(named);
4056
+ // A maker's name, in its tags or its page address, unless the site names the maker (AGL-4065),
4057
+ // or a word one letter off it: "susuki" is a Suzuki (AGL-4092).
4058
+ const makers = aiStockMakerTerms(photo);
4059
+ if ([
4060
+ ...MAKER_STEMS
4061
+ ].some((maker)=>makers.has(maker) && !site.has(maker))) return true;
4062
+ for (const term of makers){
4063
+ const maker = term.includes(' ') ? null : fuzzyMaker(term);
4064
+ if (maker && !site.has(maker) && !site.has(term)) return true;
4065
+ }
2894
4066
  return SENSITIVE_STEMS.some((topic)=>[
2895
4067
  ...topic.words
2896
4068
  ].some((word)=>found.has(word)) && ![
@@ -2908,13 +4080,15 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2908
4080
  * the subject's two-word phrase 3, each of its other words 1, and the
2909
4081
  * site's world 1, plus a quarter when it is among the first tags.
2910
4082
  */ export function aiStockRelevance(photo, judgement) {
2911
- 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;
4083
+ 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, _photo_tags, _judgement_roles;
4084
+ var _judgement_served, _judgement_roleScene, _judgement_roleRefuse, _judgement_away, _judgement_world, _judgement_oddLeads;
2913
4085
  const hit = hitWords(photo);
2914
4086
  if (hit.rejected) return 0;
2915
4087
  if (aiStockSensitive(photo, judgement.named)) return 0;
2916
4088
  // Glass art is not pottery: a rival craft's photo names this craft's own words.
2917
4089
  for (const rival of (_judgement_rivals = judgement.rivals) != null ? _judgement_rivals : [])if (rival.words.some((word)=>hit.all.has(word)) && !rival.core.some((word)=>hit.all.has(word))) return 0;
4090
+ // A trade's thing is of the world it serves, whatever else names it (AGL-4092).
4091
+ if (((_judgement_served = judgement.served) == null ? void 0 : _judgement_served.length) && !judgement.served.some((term)=>hit.all.has(stemPhrase(term)))) return 0;
2918
4092
  const subject = stemList((_judgement_subject = judgement.subject) != null ? _judgement_subject : '');
2919
4093
  // What the slot or the brief names lets a shunned word, a room or a fragrance in.
2920
4094
  const owned = new Set([
@@ -2930,17 +4104,17 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2930
4104
  // A teacher is shown teaching: a classroom, a lecture or students among the
2931
4105
  // first tags, and never a model, a pose or a painting (AGL-3856).
2932
4106
  if (((_judgement_roleScene = judgement.roleScene) == null ? void 0 : _judgement_roleScene.length) || ((_judgement_roleRefuse = judgement.roleRefuse) == null ? void 0 : _judgement_roleRefuse.length)) {
2933
- var _photo_tags, _photo_tags1, _judgement_roleRefuse1;
4107
+ var _photo_tags1, _photo_tags2, _judgement_roleRefuse1;
2934
4108
  var _judgement_roleScene1;
2935
- const first = new Set(stemList(((_photo_tags = photo.tags) != null ? _photo_tags : []).slice(0, AI_STOCK_ROLE_TAGS_READ).join(' ')));
4109
+ const first = new Set(stemList(((_photo_tags1 = photo.tags) != null ? _photo_tags1 : []).slice(0, AI_STOCK_ROLE_TAGS_READ).join(' ')));
2936
4110
  if (((_judgement_roleScene1 = judgement.roleScene) == null ? void 0 : _judgement_roleScene1.length) && !judgement.roleScene.some((stem)=>first.has(stem))) return 0;
2937
- const read = new Set(stemList(((_photo_tags1 = photo.tags) != null ? _photo_tags1 : []).slice(0, ALT_TAGS_READ).join(' ')));
4111
+ const read = new Set(stemList(((_photo_tags2 = photo.tags) != null ? _photo_tags2 : []).slice(0, ALT_TAGS_READ).join(' ')));
2938
4112
  if (((_judgement_roleRefuse1 = judgement.roleRefuse) != null ? _judgement_roleRefuse1 : []).some((stem)=>read.has(stem))) return 0;
2939
4113
  }
2940
4114
  // A Blue Ridge couple is never on a beach, however its later tags name a mountain (AGL-3942).
2941
4115
  if ((_judgement_away = judgement.away) == null ? void 0 : _judgement_away.length) {
2942
- var _photo_tags2;
2943
- const read = new Set(stemList(((_photo_tags2 = photo.tags) != null ? _photo_tags2 : []).slice(0, ALT_TAGS_READ).join(' ')));
4116
+ var _photo_tags3;
4117
+ const read = new Set(stemList(((_photo_tags3 = photo.tags) != null ? _photo_tags3 : []).slice(0, ALT_TAGS_READ).join(' ')));
2944
4118
  if (judgement.away.some((stem)=>read.has(stem))) return 0;
2945
4119
  }
2946
4120
  // A wedding day shows its people: a close-up of roses tagged "wedding" never fills it.
@@ -2965,7 +4139,8 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2965
4139
  const kinNamed = kin.some((phrase)=>among(hit.phrases, phrase));
2966
4140
  const kinLead = kin.some((phrase)=>among(hit.leadPhrases, phrase));
2967
4141
  const required = stemList((_judgement_requires = judgement.requires) != null ? _judgement_requires : '');
2968
- const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).flatMap(stemList);
4142
+ // A two-word name stays whole: "dirt bike", never "bike" (AGL-4065).
4143
+ const category = ((_judgement_category = judgement.category) != null ? _judgement_category : []).map(stemPhrase).filter(Boolean);
2969
4144
  const anyOf = category.length ? [
2970
4145
  ...new Set([
2971
4146
  ...required,
@@ -2978,21 +4153,36 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
2978
4153
  if (demanded && !kinNamed && !meets) return 0;
2979
4154
  const object = (_judgement_object = judgement.object) != null ? _judgement_object : judgement.subject ? aiStockObjectWord(judgement.subject) : '';
2980
4155
  const domain = ((_judgement_domain = judgement.domain) != null ? _judgement_domain : []).filter((stem)=>stem !== object);
4156
+ // The business's own world, which a picture that is not of a thing names (AGL-4065).
4157
+ const own = ((_judgement_world = judgement.world) == null ? void 0 : _judgement_world.length) ? judgement.world.filter((stem)=>stem !== object) : domain;
2981
4158
  // A hit naming the word it must name is of the site's world: a product's
2982
4159
  // category is that world even where it is the product's own object.
2983
4160
  const world = demanded || kinNamed || domain.some((stem)=>hit.all.has(stem));
4161
+ const ownWorld = demanded || kinNamed || own.some((stem)=>hit.all.has(stem));
2984
4162
  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
4163
  if (judgement.leadWorld && !worldLead) return 0;
4164
+ // One of its first tags is OF the business's own world (AGL-4092): "tacos",
4165
+ // "street tacos", "dirt bike" lead with it; "taco bowl", "body part" and a
4166
+ // man posing with a "motorcycle" for a motocross team do not.
4167
+ if (judgement.leadOwn && own.length && !kinLead && !leadsWithWorld((_photo_tags = photo.tags) != null ? _photo_tags : [], new Set(own), new Set(domain))) return 0;
4168
+ // A cat, a toy or a bare foot leads it, and nothing of the business's own world does (AGL-4092).
4169
+ if (((_judgement_oddLeads = judgement.oddLeads) == null ? void 0 : _judgement_oddLeads.length) && own.length) {
4170
+ var _photo_tags4;
4171
+ const first = termsOf(((_photo_tags4 = photo.tags) != null ? _photo_tags4 : []).slice(0, 3));
4172
+ if (judgement.oddLeads.some((stem)=>first.has(stem)) && !own.some((stem)=>first.has(stem))) return 0;
4173
+ }
2986
4174
  // The first tag alone, never the page's address: Pixabay's slug repeats a
2987
4175
  // late tag ("coffee-latte-art-candle-…"). A site whose words name no world
2988
4176
  // holds its hero to none.
4177
+ // The business's own world, never every word of its type (AGL-4065): "race
4178
+ // car" named "race" of "Amateur motocross race team: … race schedule".
2989
4179
  const worldStems = new Set([
2990
4180
  ...anyOf,
2991
4181
  ...required,
2992
- ...domain
4182
+ ...own
2993
4183
  ]);
2994
4184
  if (judgement.first && (worldStems.size || kin.length)) {
2995
- const firstWorld = hit.firstPhrase.split(' ').some((stem)=>worldStems.has(stem)) || kin.some((phrase)=>among([
4185
+ const firstWorld = withPairs(hit.firstPhrase.split(' ').filter(Boolean)).some((stem)=>worldStems.has(stem)) || kin.some((phrase)=>among([
2996
4186
  hit.firstPhrase
2997
4187
  ], phrase));
2998
4188
  if (!firstWorld) return 0;
@@ -3011,14 +4201,25 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
3011
4201
  const leading = kin.length ? kinLead : hit.lead.has(object);
3012
4202
  // A lifestyle scene whose object is a last tag is not a photo OF it.
3013
4203
  if (judgement.lead && !leading) return 0;
3014
- if (domain.length && !world && !phrased) return 0;
4204
+ // The business's own world, never any word of its type (AGL-4092): a cat
4205
+ // tagged "hungry" named "breakfast", and a European alley "street", of
4206
+ // "Taco food trailer: street and breakfast tacos".
4207
+ if (own.length && !ownWorld && !phrased) return 0;
3015
4208
  score += leading ? 4 : 2;
3016
4209
  if (kinNamed) score += 3;
3017
4210
  if (firstNamed(kin.length ? kin : [
3018
4211
  object
3019
4212
  ])) score += 1;
3020
- } else if (domain.length) {
3021
- if (!world) return 0;
4213
+ // A product's photo is OF it: named by its first or second tag, over one naming it fourth (AGL-4092).
4214
+ if (judgement.product) {
4215
+ var _photo_tags5;
4216
+ const firstTwo = ((_photo_tags5 = photo.tags) != null ? _photo_tags5 : []).slice(0, 2).map((tag)=>stemList(tag).join(' '));
4217
+ if ((kin.length ? kin : [
4218
+ object
4219
+ ]).some((phrase)=>among(firstTwo, phrase))) score += 2;
4220
+ }
4221
+ } else if (own.length) {
4222
+ if (!ownWorld) return 0;
3022
4223
  }
3023
4224
  if (leadAny.length && firstNamed(leadAny)) score += 1;
3024
4225
  if (phrased) score += 3;
@@ -3032,7 +4233,7 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
3032
4233
  // attorney" over "lawyers, personal injury, accident" (AGL-3856): a second
3033
4234
  // of them naming its world says it is of that world, not of something else.
3034
4235
  if (judgement.first && worldStems.size) {
3035
- const said = hit.leadPhrases.slice(0, -1).filter((phrase)=>phrase.split(' ').some((stem)=>worldStems.has(stem)));
4236
+ const said = hit.leadPhrases.slice(0, -1).filter((phrase)=>withPairs(phrase.split(' ')).some((stem)=>worldStems.has(stem)));
3036
4237
  if (said.length >= 2) score += 0.25;
3037
4238
  }
3038
4239
  return Math.max(score, 0.5);
@@ -3076,13 +4277,15 @@ const clip = (query)=>query.replace(/\s+/g, ' ').trim().slice(0, STOCK_PHOTO_QUE
3076
4277
  * A search's ranked hits with those of a shoot no slot shows yet first among
3077
4278
  * the best (AGL-3856): a team's three cards are three people where the
3078
4279
  * library has them, never one model three times while another fits as well.
3079
- * Every other order is kept.
3080
- */ export function aiStockFreshShootsFirst(ranked, shown, judgement) {
4280
+ * Every other order is kept. A product's are of a shoot no slot shows yet
4281
+ * first among ALL its accepted hits (`whole`, AGL-4092): Berm & Bolt's two
4282
+ * tires were one contributor's shoot, 32 uploads apart.
4283
+ */ export function aiStockFreshShootsFirst(ranked, shown, judgement, options = {}) {
3081
4284
  if (!shown.length || ranked.length < 2) return [
3082
4285
  ...ranked
3083
4286
  ];
3084
4287
  const best = aiStockRelevance(ranked[0], judgement);
3085
- const top = ranked.filter((photo)=>aiStockRelevance(photo, judgement) === best);
4288
+ const top = options.whole ? ranked : ranked.filter((photo)=>aiStockRelevance(photo, judgement) === best);
3086
4289
  const fresh = top.filter((photo)=>!shown.some((other)=>aiStockSameShoot(photo, other)));
3087
4290
  if (!fresh.length || fresh[0] === ranked[0]) return [
3088
4291
  ...ranked
@@ -3217,7 +4420,8 @@ function jobPhotos(key) {
3217
4420
  /**
3218
4421
  * Makers' names a tag gives, never what a photo shows to someone who cannot
3219
4422
  * 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');
4423
+ */ 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), and its gear's and tires' (AGL-4092).
4424
+ 'yamaha kawasaki suzuki ktm ducati harley harley-davidson husqvarna gasgas nissan subaru mazda chevy dodge reebok ' + `${GEAR_MAKER_WORDS.join(' ')} ` + 'petri zenit praktica yashica minolta mamiya rolleiflex voigtlander contax ricoh konica lomo lomography holga zeiss');
3221
4425
  /** Stopwords a description still says, as the people a photo shows: "a family", "a person". */ const SAID_PEOPLE = words('people person family');
3222
4426
  /** 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
4427
  /** "-ing" words that are things, and take no article: "clothing". */ const UNCOUNTED_ING = words('clothing bedding frosting icing flooring stuffing seasoning filling');
@@ -3658,6 +4862,9 @@ const EXTENSIONS = {
3658
4862
  'kin',
3659
4863
  'lead',
3660
4864
  'leadWorld',
4865
+ 'leadOwn',
4866
+ 'oddLeads',
4867
+ 'served',
3661
4868
  'first',
3662
4869
  'category',
3663
4870
  'strict',
@@ -4339,13 +5546,16 @@ const EXTENSIONS = {
4339
5546
  const base = aiStockSiteTerms(entry.subject);
4340
5547
  if (!((_entry_kin = entry.kin) == null ? void 0 : _entry_kin.length)) return base;
4341
5548
  const kin = entry.kin.map(aiStockStem);
5549
+ const domain = [
5550
+ ...new Set([
5551
+ ...base.domain,
5552
+ ...kin
5553
+ ])
5554
+ ];
4342
5555
  return _extends({}, base, {
4343
- domain: [
4344
- ...new Set([
4345
- ...base.domain,
4346
- ...kin
4347
- ])
4348
- ],
5556
+ domain,
5557
+ // The kind's own world is all of it: its subject and its kin (AGL-4065).
5558
+ lead: domain,
4349
5559
  broad: [
4350
5560
  ...(_entry_broad = entry.broad) != null ? _entry_broad : base.broad
4351
5561
  ],
@@ -4880,7 +6090,7 @@ const WORKSPACE = {
4880
6090
  * "freelencing", which no library tags, so its cover hero searched it, found
4881
6091
  * nothing, and shipped a flat dark band with no photo.
4882
6092
  */ export function aiStockWiderTerms(terms, kind) {
4883
- var _STARTER_KINDS_starter, _aiStockKindGenericTerms, _STARTER_GENERIC_KIND_starter;
6093
+ var _STARTER_KINDS_starter, _STARTER_GENERIC_KIND_starter;
4884
6094
  const rungs = [];
4885
6095
  const seen = new Set([
4886
6096
  JSON.stringify([
@@ -4890,8 +6100,11 @@ const WORKSPACE = {
4890
6100
  ]
4891
6101
  ])
4892
6102
  ]);
4893
- const add = (rung, found)=>{
6103
+ // An outdoor or a trade world never widens to an office's photos (AGL-4065).
6104
+ const outdoor = aiStockWorldIsOutdoorOrTrade(terms);
6105
+ const add = (rung, of, found)=>{
4894
6106
  if (!found || !found.broad.length) return;
6107
+ if (outdoor && OFFICE_KINDS.has(of)) return;
4895
6108
  const key = JSON.stringify([
4896
6109
  found.craft,
4897
6110
  found.broad
@@ -4904,15 +6117,42 @@ const WORKSPACE = {
4904
6117
  });
4905
6118
  };
4906
6119
  if (!aiStockWorldIsUnlisted(terms)) {
4907
- if (kind && worldOfKind(terms, kind)) add('generic', aiStockKindGenericTerms(kind));
6120
+ if (kind && worldOfKind(terms, kind)) add('generic', kind, aiStockKindGenericTerms(kind));
4908
6121
  return rungs;
4909
6122
  }
4910
6123
  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'));
6124
+ const starterKind = (_STARTER_KINDS_starter = STARTER_KINDS[starter]) != null ? _STARTER_KINDS_starter : 'business';
6125
+ if (kind) add('kind', kind, aiStockKindTerms(kind));
6126
+ add('starter', starterKind, aiStockKindTerms(starterKind));
6127
+ const generic = KIND_GENERIC[kind] ? kind : (_STARTER_GENERIC_KIND_starter = STARTER_GENERIC_KIND[starter]) != null ? _STARTER_GENERIC_KIND_starter : 'business';
6128
+ add('generic', generic, aiStockKindGenericTerms(generic));
4914
6129
  return rungs;
4915
6130
  }
6131
+ /**
6132
+ * The kinds of site whose photos are an office, a desk or a workspace
6133
+ * (AGL-4065): what a landscaper, a roofer or a taco trailer never widens to.
6134
+ */ const OFFICE_KINDS = new Set([
6135
+ 'business',
6136
+ 'landing',
6137
+ 'professional',
6138
+ 'personal',
6139
+ 'coming-soon',
6140
+ 'studio'
6141
+ ]);
6142
+ /**
6143
+ * Words of a world whose work is done outdoors, on a vehicle, with tools or
6144
+ * over food (AGL-4065), as stems: such a world's pictures are never an
6145
+ * office's. The Bluebonnet Yard & Landscape start, a world the module did not
6146
+ * list, picked as Business & services, opened every page on an office.
6147
+ */ 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));
6148
+ /** 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) {
6149
+ var _terms_lead;
6150
+ const own = [
6151
+ ...(_terms_lead = terms.lead) != null ? _terms_lead : terms.domain,
6152
+ ...stemList(`${terms.business} ${terms.craft}`)
6153
+ ];
6154
+ return own.some((term)=>term.split(' ').some((stem)=>OUTDOOR_AND_TRADE_STEMS.has(stem)));
6155
+ }
4916
6156
  /**
4917
6157
  * The first word of a text that names a craft {@link CRAFT_KIN} lists, as
4918
6158
  * its table writes it: "gym" of "Synchronicity Gym", "law" of "Hartwell
@@ -4949,6 +6189,147 @@ const WORKSPACE = {
4949
6189
  if (!aiLayoutSpeaksOtherThanEnglish(input.language)) return '';
4950
6190
  return aiStockBusinessNoun(input.business) || aiStockPhotoWordsSubject((_input_photoWords = input.photoWords) != null ? _input_photoWords : '') || ((_input_kindWords = input.kindWords) != null ? _input_kindWords : '').trim();
4951
6191
  }
6192
+ /**
6193
+ * How a search's hits are judged for a slot (AGL-3660), as the source judges
6194
+ * them: by the search's own words, held to the world it was asked for — the
6195
+ * site's own (`site`), or a wider one where the site's found nothing
6196
+ * (`world`, AGL-3923). What lets a sensitive topic, an animal or a body part
6197
+ * in is always the site's own words. On a site for adults alone no hit names
6198
+ * a child; a portrait of one of the business's people names an adult, never
6199
+ * leads with a child, and is preferred in its role (AGL-3856). Any other
6200
+ * picture never leads with an animal, a toy or a bare body part its words
6201
+ * and the brief do not name, unless its first tags name the business's own
6202
+ * world too (AGL-4092): Pit Row Tacos' "Come hungry" took a cat. Pure.
6203
+ */ export function aiStockJudgementFor(slot, request, subject, site, sectionName = '', world = site) {
6204
+ var _request_refuse, _ref, _request_subject, _request_shun, _request_refuseLead;
6205
+ var _slot_product, _world_lead, _request_allow, _request_shun1, _request_leadAny, _request_served, _request_needs, _request_refuseLead1, _request_away, _request_needs1, _role_scene, _role_refuse;
6206
+ const refuse = [
6207
+ ...(_request_refuse = request.refuse) != null ? _request_refuse : [],
6208
+ ...site.adults ? CHILD_STEMS : []
6209
+ ];
6210
+ const role = slot.people ? aiStockPeopleRole(slot.alt, sectionName) : null;
6211
+ // A portrait's face is its own, and a craft whose pictures are people at a moment shows their hands and rings (AGL-4092).
6212
+ const oddLeads = slot.people ? [] : aiStockOddLeads([
6213
+ slot.alt,
6214
+ ...(_ref = (_slot_product = slot.product) == null ? void 0 : _slot_product.subjects) != null ? _ref : [],
6215
+ sectionName,
6216
+ ...site.named
6217
+ ], {
6218
+ body: slot.role !== 'about' && !site.scenes
6219
+ });
6220
+ return _extends({
6221
+ subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
6222
+ domain: world.domain
6223
+ }, ((_world_lead = world.lead) == null ? void 0 : _world_lead.length) ? {
6224
+ world: world.lead
6225
+ } : {}, {
6226
+ strict: slot.role === 'gallery' && !request.broad || !!request.requires || !!request.strict,
6227
+ named: ((_request_allow = request.allow) == null ? void 0 : _request_allow.length) ? [
6228
+ ...new Set([
6229
+ ...site.named,
6230
+ ...request.allow
6231
+ ])
6232
+ ] : site.named,
6233
+ rivals: world.rivals
6234
+ }, world.shun.length || ((_request_shun1 = request.shun) == null ? void 0 : _request_shun1.length) ? {
6235
+ shun: [
6236
+ ...new Set([
6237
+ ...world.shun,
6238
+ ...(_request_shun = request.shun) != null ? _request_shun : []
6239
+ ])
6240
+ ]
6241
+ } : {}, world.shunLead.length ? {
6242
+ shunLead: world.shunLead
6243
+ } : {}, request.product ? {
6244
+ product: true
6245
+ } : {}, ((_request_leadAny = request.leadAny) == null ? void 0 : _request_leadAny.length) ? {
6246
+ leadAny: request.leadAny
6247
+ } : {}, request.requires ? {
6248
+ requires: request.requires
6249
+ } : {}, request.category ? {
6250
+ category: request.category
6251
+ } : {}, request.object ? {
6252
+ object: request.object
6253
+ } : {}, request.kin ? {
6254
+ kin: request.kin
6255
+ } : {}, request.lead ? {
6256
+ lead: true
6257
+ } : {}, request.leadWorld ? {
6258
+ leadWorld: true
6259
+ } : {}, request.leadOwn ? {
6260
+ leadOwn: true
6261
+ } : {}, ((_request_served = request.served) == null ? void 0 : _request_served.length) ? {
6262
+ served: request.served
6263
+ } : {}, request.first ? {
6264
+ first: true
6265
+ } : {}, refuse.length ? {
6266
+ refuse: [
6267
+ ...new Set(refuse)
6268
+ ]
6269
+ } : {}, ((_request_needs = request.needs) == null ? void 0 : _request_needs.length) ? {
6270
+ needs: request.needs
6271
+ } : {}, ((_request_refuseLead1 = request.refuseLead) == null ? void 0 : _request_refuseLead1.length) ? {
6272
+ refuseLead: request.refuseLead
6273
+ } : {}, ((_request_away = request.away) == null ? void 0 : _request_away.length) ? {
6274
+ away: request.away
6275
+ } : {}, oddLeads.length ? {
6276
+ oddLeads
6277
+ } : {}, request.worldless ? {
6278
+ domain: [],
6279
+ rivals: []
6280
+ } : {}, slot.people ? _extends({
6281
+ needs: ((_request_needs1 = request.needs) == null ? void 0 : _request_needs1.length) ? request.needs : (role == null ? void 0 : role.required) ? role.kin : ADULT_PEOPLE_STEMS,
6282
+ refuseLead: [
6283
+ ...new Set([
6284
+ ...CHILD_STEMS,
6285
+ ...(_request_refuseLead = request.refuseLead) != null ? _request_refuseLead : []
6286
+ ])
6287
+ ]
6288
+ }, role ? {
6289
+ roles: role.kin
6290
+ } : {}, (role == null ? void 0 : (_role_scene = role.scene) == null ? void 0 : _role_scene.length) ? {
6291
+ roleScene: role.scene
6292
+ } : {}, (role == null ? void 0 : (_role_refuse = role.refuse) == null ? void 0 : _role_refuse.length) ? {
6293
+ roleRefuse: aiStockRoleRefusals(role, slot.alt)
6294
+ } : {}) : {});
6295
+ }
6296
+ /**
6297
+ * The craft's own photos, the last thing a slot of a craft's site is filled
6298
+ * from (AGL-3660): its broad searches, any hit naming the craft itself. Never
6299
+ * a desk, a laptop or a living room for a soap shop. A picture that is not a
6300
+ * hero nor a product takes only a photo OF the business's own world among its
6301
+ * first tags (AGL-4092): a taco trailer's "Race day" took a chicken salad
6302
+ * tagged "taco bowl". Pure.
6303
+ */ export function aiStockCraftSearches(slot, world) {
6304
+ // A portfolio's record of the work is never filled by the craft's own
6305
+ // photos (AGL-3856), nor is any picture of a site that shows its work (AGL-3942).
6306
+ if (!world.core.length || aiStockShowsTheWork(slot, world) || aiStockShowsItsWork(slot, world)) return [];
6307
+ const orientation = aiStockOrientation(slot.aspect);
6308
+ const size = slot.role === 'hero' ? {
6309
+ minWidth: 1600
6310
+ } : orientation === 'vertical' ? {
6311
+ minHeight: 900
6312
+ } : {
6313
+ minWidth: 900
6314
+ };
6315
+ return world.broad.map((query)=>{
6316
+ var _world_held;
6317
+ return _extends({
6318
+ query: clip(query),
6319
+ orientation
6320
+ }, size, {
6321
+ broad: true,
6322
+ category: world.core
6323
+ }, slot.role === 'hero' ? {
6324
+ first: true
6325
+ } : slot.product && !((_world_held = world.held) == null ? void 0 : _world_held.length) ? {} : {
6326
+ leadOwn: true
6327
+ }, slot.product ? {
6328
+ product: true,
6329
+ tier: 'weak'
6330
+ } : {});
6331
+ });
6332
+ }
4952
6333
  /**
4953
6334
  * The source for one page, or `null` when this deployment has no stock
4954
6335
  * photo library or no media door, which leaves every slot to the starters.
@@ -5126,120 +6507,10 @@ const WORKSPACE = {
5126
6507
  * hit names a child; a portrait of one of the business's people names an
5127
6508
  * adult, never leads with a child, and is preferred in its role (AGL-3856).
5128
6509
  */ const judgementFor = (slot, request, subject)=>{
5129
- var _request_refuse, _input_sectionNames_slot_sectionIndex, _worldOf_get, _request_refuseLead;
5130
- var _request_needs, _role_scene, _role_refuse;
5131
- const refuse = [
5132
- ...(_request_refuse = request.refuse) != null ? _request_refuse : [],
5133
- ...terms.adults ? CHILD_STEMS : []
5134
- ];
5135
- const role = slot.people ? aiStockPeopleRole(slot.alt, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '') : null;
5136
- return _extends({}, judgementOf(slot, request, subject, (_worldOf_get = worldOf.get(request)) != null ? _worldOf_get : terms), refuse.length ? {
5137
- refuse: [
5138
- ...new Set(refuse)
5139
- ]
5140
- } : {}, slot.people ? _extends({
5141
- needs: ((_request_needs = request.needs) == null ? void 0 : _request_needs.length) ? request.needs : (role == null ? void 0 : role.required) ? role.kin : ADULT_PEOPLE_STEMS,
5142
- refuseLead: [
5143
- ...new Set([
5144
- ...CHILD_STEMS,
5145
- ...(_request_refuseLead = request.refuseLead) != null ? _request_refuseLead : []
5146
- ])
5147
- ]
5148
- }, role ? {
5149
- roles: role.kin
5150
- } : {}, (role == null ? void 0 : (_role_scene = role.scene) == null ? void 0 : _role_scene.length) ? {
5151
- roleScene: role.scene
5152
- } : {}, (role == null ? void 0 : (_role_refuse = role.refuse) == null ? void 0 : _role_refuse.length) ? {
5153
- roleRefuse: aiStockRoleRefusals(role, slot.alt)
5154
- } : {}) : {});
5155
- };
5156
- /**
5157
- * A search's own judgement, as its words set it, held to the world it was
5158
- * asked for: the site's own, or a wider one where the site's found nothing
5159
- * (AGL-3923). What lets a sensitive topic in is always the site's own words.
5160
- */ const judgementOf = (slot, request, subject, world)=>{
5161
- var _request_subject, _request_shun;
5162
- var _request_allow, _request_shun1, _request_leadAny, _request_refuse, _request_needs, _request_refuseLead, _request_away;
5163
- return _extends({
5164
- subject: (_request_subject = request.subject) != null ? _request_subject : request.broad ? '' : subject,
5165
- domain: world.domain,
5166
- strict: slot.role === 'gallery' && !request.broad || !!request.requires || !!request.strict,
5167
- named: ((_request_allow = request.allow) == null ? void 0 : _request_allow.length) ? [
5168
- ...new Set([
5169
- ...terms.named,
5170
- ...request.allow
5171
- ])
5172
- ] : terms.named,
5173
- rivals: world.rivals
5174
- }, world.shun.length || ((_request_shun1 = request.shun) == null ? void 0 : _request_shun1.length) ? {
5175
- shun: [
5176
- ...new Set([
5177
- ...world.shun,
5178
- ...(_request_shun = request.shun) != null ? _request_shun : []
5179
- ])
5180
- ]
5181
- } : {}, world.shunLead.length ? {
5182
- shunLead: world.shunLead
5183
- } : {}, request.product ? {
5184
- product: true
5185
- } : {}, ((_request_leadAny = request.leadAny) == null ? void 0 : _request_leadAny.length) ? {
5186
- leadAny: request.leadAny
5187
- } : {}, request.requires ? {
5188
- requires: request.requires
5189
- } : {}, request.category ? {
5190
- category: request.category
5191
- } : {}, request.object ? {
5192
- object: request.object
5193
- } : {}, request.kin ? {
5194
- kin: request.kin
5195
- } : {}, request.lead ? {
5196
- lead: true
5197
- } : {}, request.leadWorld ? {
5198
- leadWorld: true
5199
- } : {}, request.first ? {
5200
- first: true
5201
- } : {}, ((_request_refuse = request.refuse) == null ? void 0 : _request_refuse.length) ? {
5202
- refuse: request.refuse
5203
- } : {}, ((_request_needs = request.needs) == null ? void 0 : _request_needs.length) ? {
5204
- needs: request.needs
5205
- } : {}, ((_request_refuseLead = request.refuseLead) == null ? void 0 : _request_refuseLead.length) ? {
5206
- refuseLead: request.refuseLead
5207
- } : {}, ((_request_away = request.away) == null ? void 0 : _request_away.length) ? {
5208
- away: request.away
5209
- } : {}, request.worldless ? {
5210
- domain: [],
5211
- rivals: []
5212
- } : {});
5213
- };
5214
- /**
5215
- * The craft's own photos, the last thing a slot of a craft's site is
5216
- * filled from (AGL-3660): its broad searches, any hit naming the craft
5217
- * itself. Never a desk, a laptop or a living room for a soap shop.
5218
- */ const craftSearches = (slot, world = terms)=>{
5219
- // A portfolio's record of the work is never filled by the craft's own
5220
- // photos (AGL-3856), nor is any picture of a site that shows its work (AGL-3942).
5221
- if (!world.core.length || aiStockShowsTheWork(slot, world) || aiStockShowsItsWork(slot, world)) return [];
5222
- const orientation = aiStockOrientation(slot.aspect);
5223
- const size = slot.role === 'hero' ? {
5224
- minWidth: 1600
5225
- } : orientation === 'vertical' ? {
5226
- minHeight: 900
5227
- } : {
5228
- minWidth: 900
5229
- };
5230
- return world.broad.map((query)=>_extends({
5231
- query: clip(query),
5232
- orientation
5233
- }, size, {
5234
- broad: true,
5235
- category: world.core
5236
- }, slot.role === 'hero' ? {
5237
- first: true
5238
- } : {}, slot.product ? {
5239
- product: true,
5240
- tier: 'weak'
5241
- } : {}));
6510
+ var _input_sectionNames_slot_sectionIndex, _worldOf_get;
6511
+ return aiStockJudgementFor(slot, request, subject, terms, (_input_sectionNames_slot_sectionIndex = input.sectionNames[slot.sectionIndex]) != null ? _input_sectionNames_slot_sectionIndex : '', (_worldOf_get = worldOf.get(request)) != null ? _worldOf_get : terms);
5242
6512
  };
6513
+ const craftSearches = (slot, world = terms)=>aiStockCraftSearches(slot, world);
5243
6514
  /**
5244
6515
  * A slot's searches once its own world found it nothing (AGL-3923), in
5245
6516
  * order: the kind of site's world, the starter's, the slot's own words
@@ -5259,10 +6530,16 @@ const WORKSPACE = {
5259
6530
  };
5260
6531
  for (const { rung, terms: world } of wider){
5261
6532
  if (rung === 'generic') continue;
6533
+ // A wider world is the site's only as a kind of site: its photos are held to it as before (AGL-4092).
5262
6534
  ask(world, [
5263
6535
  ...aiStockSearchesFor(slot, world, subject, part, sectionName),
5264
6536
  ...craftSearches(slot, world)
5265
- ]);
6537
+ ].map((_0)=>{
6538
+ let { leadOwn: _own } = _0, request = _object_without_properties_loose(_0, [
6539
+ "leadOwn"
6540
+ ]);
6541
+ return request;
6542
+ }));
5266
6543
  }
5267
6544
  const orientation = aiStockOrientation(slot.aspect);
5268
6545
  const size = slot.role === 'hero' ? {
@@ -5325,15 +6602,21 @@ const WORKSPACE = {
5325
6602
  const subject = english ? '' : aiStockSubjectWords(slot.alt, sectionName);
5326
6603
  let choice = null;
5327
6604
  const requests = [
5328
- ...slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain, {
6605
+ ...slot.product ? aiStockProductSearches(slot, terms.craft, terms.domain, _extends({
5329
6606
  broad: terms.broad,
5330
6607
  core: terms.core
5331
- }) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName), sectionName),
6608
+ }, terms.held ? {
6609
+ held: terms.held
6610
+ } : {}, terms.leadWord ? {
6611
+ leadWord: terms.leadWord
6612
+ } : {})) : aiStockSearchesFor(slot, terms, subject, aiStockSlotPart(slot, sectionName), sectionName),
5332
6613
  ...craftSearches(slot)
5333
6614
  ];
6615
+ /** Whether the library answered any of the slot's own searches with a photo at all (AGL-4065). */ let knew = false;
5334
6616
  /** The slot's accepted hits for a search; `null` once the page's searches are spent. */ const accepted = async (request, repeat)=>{
5335
6617
  const found = await search(request);
5336
6618
  if (found === null) return null;
6619
+ if (found.length && !worldOf.has(request)) knew = true;
5337
6620
  const blocked = new Set();
5338
6621
  if (!repeat) {
5339
6622
  for (const key of claimed)blocked.add(key);
@@ -5349,8 +6632,12 @@ const WORKSPACE = {
5349
6632
  }
5350
6633
  const judgement = judgementFor(slot, request, subject);
5351
6634
  const ranked = aiStockRank(found, blocked, `${input.seed}:${index}:${request.query}`, judgement);
5352
- // A team's portraits are of different people where the hits allow (AGL-3856).
5353
- return slot.people && !repeat ? aiStockFreshShootsFirst(ranked, shown, judgement) : ranked;
6635
+ // A team's portraits are of different people where the hits allow (AGL-3856),
6636
+ // and a shop's products of different shoots (AGL-4092).
6637
+ if (repeat || !(slot.people || slot.product)) return ranked;
6638
+ return aiStockFreshShootsFirst(ranked, shown, judgement, slot.product ? {
6639
+ whole: true
6640
+ } : {});
5354
6641
  };
5355
6642
  /** The first of these searches with a hit to take: a new one, or one the job already shows. */ const choose = async (tried, repeat)=>{
5356
6643
  for (const request of tried){
@@ -5383,44 +6670,54 @@ const WORKSPACE = {
5383
6670
  const sure = slot.product ? requests.filter((request)=>request.tier !== 'weak') : requests;
5384
6671
  const unsure = slot.product ? requests.filter((request)=>request.tier === 'weak') : [];
5385
6672
  // 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.
6673
+ // and then, for a hero, one the job already shows. A listed world
6674
+ // widens only where the library knew nothing of it (AGL-4065): one
6675
+ // it answered with photos it turned down keeps its slot without a
6676
+ // photo, never an office. A world of its own words alone still widens.
5387
6677
  const widened = widerSearches(slot, subject, sectionName);
5388
6678
  const phases = [
5389
6679
  [
5390
6680
  sure,
6681
+ false,
5391
6682
  false
5392
6683
  ],
5393
6684
  ...repeats ? [
5394
6685
  [
5395
6686
  sure,
5396
- true
6687
+ true,
6688
+ false
5397
6689
  ]
5398
6690
  ] : [],
5399
6691
  ...unsure.length ? [
5400
6692
  [
5401
6693
  unsure,
6694
+ false,
5402
6695
  false
5403
6696
  ],
5404
6697
  [
5405
6698
  unsure,
5406
- true
6699
+ true,
6700
+ false
5407
6701
  ]
5408
6702
  ] : [],
5409
6703
  ...widened.length ? [
5410
6704
  [
5411
6705
  widened,
5412
- false
6706
+ false,
6707
+ true
5413
6708
  ]
5414
6709
  ] : [],
5415
6710
  ...widened.length && repeats ? [
5416
6711
  [
5417
6712
  widened,
6713
+ true,
5418
6714
  true
5419
6715
  ]
5420
6716
  ] : []
5421
6717
  ];
5422
- for (const [tried, repeat] of phases){
6718
+ for (const [tried, repeat, wide] of phases){
5423
6719
  if (choice || signal.aborted) break;
6720
+ if (wide && knew && !unlisted) break;
5424
6721
  choice = await choose(tried, repeat);
5425
6722
  }
5426
6723
  choices.push(choice);