{"description":"Searches local.ch, the Swiss business and phone directory, for a trade in a place: business name, categories, address, public phone number, map position, whether it is open now, and a link to the entry, 20 per page with the total number of matches. The trade must be a category local.ch knows; free-text terms go to its full-text search, which this recipe does not use (the run then fails with a redirect).","effect":"read","id":"local.ch/search-businesses","keywords":["local.ch","telefonbuch","branchen","firmen","adresse","telefonnummer","öffnungszeiten","zahnarzt","schweiz","zürich","bern","annuaire","local business","directory","switzerland"],"output":{"schema":{"properties":{"businesses":{"items":{"properties":{"address":{"description":"Street, postcode and place","type":["string","null"]},"categories":{"description":"e.g. 'Zahnarzt • Dentalhygiene'","type":["string","null"]},"latitude":{"type":["number","null"]},"longitude":{"type":["number","null"]},"name":{"type":"string"},"open_now":{"description":"From the card badge; null when the card shows none","type":["boolean","null"]},"opening":{"description":"Badge text, e.g. 'Geschlossen', 'Geöffnet'","type":["string","null"]},"phone":{"description":"The business's public number, international format","type":["string","null"]},"url":{"type":"string"}},"type":"object"},"type":"array"},"total":{"type":["integer","null"]},"what":{"type":"string"},"where":{"type":"string"}},"required":["what","where","businesses"],"type":"object"},"value":"let $s = steps.search[0], $cards = steps.search[0].cards || `[]` in {what: params.what, where: params.where, total: ($s.count && to_number(replace(split($s.count, ' ')[0], '\\'', ''))) || `0`, businesses: map(&(let $p = @ in let $c = $cards[?join('', ['https://www.local.ch', href]) == $p.url] | [0] in {name: $p.name, categories: $c.categories || `null`, address: join(', ', [$p.street || '', join(' ', [$p.zip || '', $p.city || ''])]), phone: $p.phone || `null`, open_now: (`[[\"xenon-theme__danger\", false], [\"xenon-theme__success\", true]]`[?contains($c.badge_class || '', @[0])] | [0][1]), opening: $c.badge || `null`, latitude: ($p.lat && to_number($p.lat)) || `null`, longitude: ($p.lon && to_number($p.lon)) || `null`, url: $p.url}), $s.places || `[]`)}"},"params":{"additionalProperties":false,"properties":{"what":{"description":"Trade or business name in German, e.g. 'zahnarzt', 'elektriker', 'coiffeur'","maxLength":60,"minLength":2,"type":"string"},"where":{"description":"Swiss place, e.g. 'zürich', 'bern', 'basel', 'luzern'","maxLength":60,"minLength":2,"type":"string"}},"required":["what","where"],"type":"object"},"provenance":{"method":"browser-inspect","notes":"The search form's /de/q?what=zahnarzt&where=zürich answers 301 to /de/q/<where>/<what> and then 308 to the transliterated path (zürich → zuerich); some entry URLs also 307 to add an rid parameter. The recipe calls /de/q/<where>/<what> with ae/oe/ue directly, which answers 200. The page's ld+json has an ItemList of 20 LocalBusiness entries (name, url, address, telephone, geo). The cards (article.xenon-entry-card, rendered twice for two layouts, so the first 20 are taken) add the categories line (h2 + span) and a badge whose theme class is xenon-theme__danger when closed; cards are joined to the ld+json by URL. The total is the text after the h1 ('406 Ergebnisse'). A term that is not a category answers 308 to /de/s/<place>/<term>, the full-text search robots.txt disallows; the executor does not follow it, so such a run fails. No card was labelled as an ad or premium entry. Ratings are drawn as stars without text and are left out.","recorded_at":"2026-11-07T22:37:00+01:00","recorded_by":"C4"},"requires_login":false,"schema":"https://wield.run/schema/recipe/v1.json","site":{"country":"CH","id":"local.ch","language":"de","name":"local.ch","origin":"https://www.local.ch","origins":["https://www.local.ch"]},"steps":[{"description":"local.ch's search page /de/q/<place>/<what> with umlauts written ae/oe/ue (the form's /de/q?what=…&where=… answers 301 and then 308 to that path, and those redirects can add an rid parameter; calling the final path directly avoids them; places are written as the site's slugs, e.g. 'st. gallen' → st-gallen). Its ld+json lists the 20 businesses; the cards add categories and the open/closed badge.","expect":{"content_type":"html","status":[200]},"extract":{"embedded_json":{"required":false,"selector":"script[type=\"application/ld+json\"]","vars":{"places":"[?\"@type\" == 'ItemList'] | [0].itemListElement[].{name: name, url: url, street: address.streetAddress, zip: address.postalCode, city: address.addressLocality, phone: telephone, lat: geo.latitude, lon: geo.longitude}"}},"html":{"count":{"selector":"h1 + div"}},"html_items":{"cards":{"fields":{"badge":{"selector":".xenon-badge"},"badge_class":{"attr":"class","selector":".xenon-badge"},"categories":{"selector":"h2 + span"},"href":{"attr":"href","selector":"a"}},"max":20,"selector":"article.xenon-entry-card"}}},"foreach":{"as":"f","in":"[{where: replace(replace(replace(replace(replace(replace(replace(lower(trim(params.where)), 'ä', 'ae'), 'ö', 'oe'), 'ü', 'ue'), 'é', 'e'), 'è', 'e'), '.', ''), ' ', '-'), what: replace(replace(replace(replace(replace(replace(replace(lower(trim(params.what)), 'ä', 'ae'), 'ö', 'oe'), 'ü', 'ue'), 'é', 'e'), 'è', 'e'), '.', ''), ' ', '-')}]","max":1},"id":"search","request":{"headers":{"Accept":"text/html"},"method":"GET","url":"/de/q/{{f.where | urlencode}}/{{f.what | urlencode}}"}}],"task":"Find Swiss businesses by trade and place on local.ch","tos":{"notes":"robots.txt allows /de/q/ (it disallows /de/s/, map, print and route URLs) and has no rules for Claude agents or AI content signals. One request per call; no calls, reviews or bookings.","reviewed":false},"verify":{"checks":["length(output.businesses) == `20`","output.total > `100`","length(output.businesses[?starts_with(url, 'https://www.local.ch/de/d/')]) == length(output.businesses)","length(output.businesses[?phone != `null`]) > `15`","length(output.businesses[?contains(address, 'Zürich')]) > `15`","length(output.businesses[?categories != `null`]) == length(output.businesses)"],"example_params":{"what":"zahnarzt","where":"zürich"},"max_ms":4000},"version":1}