*/ public function terms(string $text): array { $parts = preg_split('/[\s,;]+/u', Str::lower(Str::ascii(trim($text)))) ?: []; return array_values(array_unique(array_filter(array_map(fn ($part) => trim($part, "\"'"), $parts), fn ($part) => $part !== '' && ! in_array($part, self::STOP_WORDS, true)))); } /** A secondary query keeps the subject/location; it never relabels processing as cultivation. */ public function relatedQuery(string $text): ?string { $terms = $this->terms($text); $subject = array_values(array_diff($terms, self::CULTIVATION)); return $subject !== [] && count($subject) < count($terms) ? implode(' ', $subject) : null; } /** @param literal-string $column * @return literal-string */ private function normalized(string $column): string { return "translate(lower(COALESCE($column, '')), 'áàâãäåéèêëíìîïóòôõöúùûüçñýÿ', 'aaaaaaeeeeiiiiooooouuuucnyy')"; } /** @param Builder $query */ public function constrain(Builder $query, string $text): void { $terms = $this->terms($text); if ($terms === []) { $query->whereRaw('false'); return; } foreach ($terms as $term) { $variants = in_array($term, self::CULTIVATION, true) ? ['plantio', 'plantacao', 'plantacoes', 'cultiv', 'semead', 'semeio'] : [$term]; $patterns = array_map(fn ($value) => '%'.addcslashes($value, '%_\\').'%', $variants); $geography = app(InvestmentGeography::class); $locations = $geography->publicReferences()->where(function ($names) use ($patterns): void { foreach ($patterns as $pattern) { $names->orWhereRaw($this->normalized('name').' LIKE ?', [$pattern]); } })->pluck('id'); $territories = []; foreach ($locations as $location) { $territories = array_merge($territories, $geography->descendants((string) $location)); } $territories = array_values(array_unique($territories)); $query->where(function ($matches) use ($patterns, $territories): void { foreach ($patterns as $pattern) { foreach (['opportunities.title', 'opportunities.description', 'opportunities.summary', 'opportunities.reference', 'opportunities.province', 'opportunities.district'] as $column) { $matches->orWhereRaw($this->normalized($column).' LIKE ?', [$pattern]); } $matches->orWhereHas('sector', fn ($sector) => $sector->whereRaw($this->normalized('name').' LIKE ?', [$pattern]) ->orWhereRaw($this->normalized('slug').' LIKE ?', [$pattern])); $matches->orWhereExists(fn ($dossier) => $dossier->selectRaw('1')->from('investment_dossiers as searchable_dossier') ->whereIn('searchable_dossier.id', InvestmentPortfolioController::visible()->reorder()->select('investment_dossiers.id')) ->where(fn ($value) => $value->whereRaw($this->normalized('searchable_dossier.title').' LIKE ?', [$pattern]) ->orWhereRaw($this->normalized('searchable_dossier.content::text').' LIKE ?', [$pattern])) ->where(fn ($relation) => $relation ->whereExists(fn ($project) => $project->selectRaw('1')->from('opportunity_projects as search_project')->whereColumn('search_project.opportunity_id', 'opportunities.id')->whereColumn('search_project.project_id', 'searchable_dossier.project_id')) ->orWhereExists(fn ($cluster) => $cluster->selectRaw('1')->from('opportunity_clusters as search_cluster')->whereColumn('search_cluster.opportunity_id', 'opportunities.id')->whereColumn('search_cluster.cluster_id', 'searchable_dossier.cluster_id')))); } if ($territories !== []) { $matches->orWhereIn('opportunities.primary_territory_id', $territories) ->orWhereExists(fn ($linked) => $linked->selectRaw('1')->from('opportunity_locations as search_link') ->whereColumn('search_link.opportunity_id', 'opportunities.id')->whereIn('search_link.geo_location_id', $territories)); } }); } } }