<?php /** * SEO geo_region : regions administratives FR (metropole + Corse). * Rattachement EXCLUSIVEMENT via numero de departement / zone deja resolu. * Pas de GPS. DOM/COM exclus. UTF-8 sans BOM. */ require_once __DIR__ . '/seo_geo_scope.php'; require_once __DIR__ . '/seo_geo_area.php'; require_once __DIR__ . '/seo_zones.php'; if (!function_exists('seo_geo_region_definitions')) { /** * @return array slug => [label, departments[]] */ function seo_geo_region_definitions() { return array( 'auvergne-rhone-alpes' => array( 'label' => 'Auvergne-Rhône-Alpes', 'departments' => array('01', '03', '07', '15', '26', '38', '42', '43', '63', '69', '73', '74'), ), 'bourgogne-franche-comte' => array( 'label' => 'Bourgogne-Franche-Comté', 'departments' => array('21', '25', '39', '58', '70', '71', '89', '90'), ), 'bretagne' => array( 'label' => 'Bretagne', 'departments' => array('22', '29', '35', '56'), ), 'centre-val-de-loire' => array( 'label' => 'Centre-Val de Loire', 'departments' => array('18', '28', '36', '37', '41', '45'), ), 'corse' => array( 'label' => 'Corse', 'departments' => array('2A', '2B'), ), 'grand-est' => array( 'label' => 'Grand Est', 'departments' => array('08', '10', '51', '52', '54', '55', '57', '67', '68', '88'), ), 'hauts-de-france' => array( 'label' => 'Hauts-de-France', 'departments' => array('02', '59', '60', '62', '80'), ), 'ile-de-france' => array( 'label' => 'Île-de-France', 'departments' => array('75', '77', '78', '91', '92', '93', '94', '95'), ), 'normandie' => array( 'label' => 'Normandie', 'departments' => array('14', '27', '50', '61', '76'), ), 'nouvelle-aquitaine' => array( 'label' => 'Nouvelle-Aquitaine', 'departments' => array('16', '17', '19', '23', '24', '33', '40', '47', '64', '79', '86', '87'), ), 'occitanie' => array( 'label' => 'Occitanie', 'departments' => array('09', '11', '12', '30', '31', '32', '34', '46', '48', '65', '66', '81', '82'), ), 'pays-de-la-loire' => array( 'label' => 'Pays de la Loire', 'departments' => array('44', '49', '53', '72', '85'), ), 'provence-alpes-cote-d-azur' => array( 'label' => 'Provence-Alpes-Côte d\'Azur', 'departments' => array('04', '05', '06', '13', '83', '84'), ), ); } } if (!function_exists('seo_geo_region_dept_map')) { /** @return array dept_num => region_slug */ function seo_geo_region_dept_map() { static $map = null; if ($map !== null) { return $map; } $map = array(); foreach (seo_geo_region_definitions() as $slug => $def) { foreach ($def['departments'] as $d) { $map[(string)$d] = $slug; } } return $map; } } if (!function_exists('seo_geo_region_pack')) { function seo_geo_region_pack($slug, $method, $confidence) { $defs = seo_geo_region_definitions(); $slug = trim((string)$slug); if ($slug === '' || !isset($defs[$slug])) { return null; } $d = $defs[$slug]; return array( 'region_id' => $slug, 'slug' => $slug, 'label' => (string)$d['label'], 'departments' => array_values($d['departments']), 'method' => (string)$method, 'confidence' => (string)$confidence, 'prep_phrase' => seo_geo_region_phrase_fr($slug), 'url_path' => '/fr/region/' . rawurlencode($slug) . '/', ); } } if (!function_exists('seo_geo_region_phrase_fr')) { /** « en Bretagne », « en Île-de-France », « en Provence-Alpes-Côte d'Azur ». */ function seo_geo_region_phrase_fr($slugOrLabel) { $slug = trim((string)$slugOrLabel); $defs = seo_geo_region_definitions(); $label = isset($defs[$slug]) ? $defs[$slug]['label'] : $slug; /* Toutes les regions FR actuelles prennent « en » */ return 'en ' . $label; } } if (!function_exists('seo_geo_region_from_department')) { /** * @param string $deptNum ex. 56, 2A * @return array|null */ function seo_geo_region_from_department($deptNum) { $z = trim((string)$deptNum); if ($z === '') { return null; } /* Normalise 1 ’! 01 */ if (ctype_digit($z) && strlen($z) === 1) { $z = '0' . $z; } if (ctype_digit($z) && (int)$z >= 1 && (int)$z <= 95) { $z = sprintf('%02d', (int)$z); } if (!function_exists('seo_geo_scope_is_metro_zone') || !seo_geo_scope_is_metro_zone($z)) { return null; } $map = seo_geo_region_dept_map(); if (!isset($map[$z])) { return null; } return seo_geo_region_pack($map[$z], 'department_table', 'high'); } } if (!function_exists('seo_resolve_region_from_area')) { /** * @param array $areaMeta seo_geo_area_pack / seo_resolve_geo_area * @return array|null */ function seo_resolve_region_from_area(array $areaMeta) { $country = isset($areaMeta['country_scope']) ? (string)$areaMeta['country_scope'] : ''; if ($country !== 'france') { return null; } $kind = isset($areaMeta['kind']) ? (string)$areaMeta['kind'] : ''; if ($kind !== 'department') { return null; } $dept = isset($areaMeta['area_id']) ? (string)$areaMeta['area_id'] : ''; if ($dept === '' && isset($areaMeta['code'])) { $dept = (string)$areaMeta['code']; } return seo_geo_region_from_department($dept); } } if (!function_exists('seo_resolve_geo_region')) { /** * Resolve region from raw spot row (via country_scope + geo_area ou port metro). * @param array $spotRow * @param array|null $countryScope * @param array|null $areaMeta * @return array|null */ function seo_resolve_geo_region($spotRow, $countryScope = null, $areaMeta = null) { if (!is_array($spotRow)) { return null; } if ($areaMeta === null) { $areaMeta = seo_resolve_geo_area($spotRow, $countryScope); } if (is_array($areaMeta)) { $reg = seo_resolve_region_from_area($areaMeta); if ($reg !== null) { return $reg; } } if ($countryScope === null) { $countryScope = function_exists('seo_resolve_country_scope') ? seo_resolve_country_scope($spotRow) : null; } if (!is_array($countryScope) || (string)$countryScope['slug'] !== 'france') { return null; } $z = isset($countryScope['zone']) ? (string)$countryScope['zone'] : ''; if ($z === '' && function_exists('psv_spot2be_port_to_zone_num')) { $z = (string)psv_spot2be_port_to_zone_num(isset($spotRow['port']) ? $spotRow['port'] : null); } return seo_geo_region_from_department($z); } } if (!function_exists('seo_geo_region_url')) { function seo_geo_region_url($lang, $slug) { $lang = function_exists('seo_normalize_lang') ? seo_normalize_lang($lang) : 'fr'; $slug = trim((string)$slug); if ($slug === '') { return function_exists('seo_lang_home') ? seo_lang_home($lang) : '/fr/'; } $path = '/' . $lang . '/region/' . rawurlencode($slug) . '/'; return function_exists('seo_absolute_url') ? seo_absolute_url($path) : $path; } } if (!function_exists('seo_geo_region_cache_path')) { function seo_geo_region_cache_path($regionSlug, $sport) { if (!function_exists('seo_stats_cache_root')) { $lib = dirname(__FILE__) . '/seo_stats_lib.php'; if (is_file($lib)) { require_once $lib; } } if (!function_exists('seo_stats_cache_root') || !function_exists('seo_stats_normalize_sport')) { return null; } $r = strtolower(preg_replace('/[^a-z0-9_-]/', '', (string)$regionSlug)); $sp = seo_stats_normalize_sport($sport); if ($r === '' || $sp === null) { return null; } return seo_stats_cache_root() . '/region/' . $r . '/' . $sp . '.json'; } } if (!function_exists('seo_geo_region_label')) { function seo_geo_region_label($slug) { $defs = seo_geo_region_definitions(); $slug = trim((string)$slug); return isset($defs[$slug]) ? (string)$defs[$slug]['label'] : $slug; } } if (!function_exists('seo_geo_region_departments_present')) { /** * Departements de la region qui ont au moins un spot dans l'index public (france). * @return string[] */ function seo_geo_region_departments_present($regionSlug) { $pack = seo_geo_region_pack($regionSlug, 'lookup', 'high'); if ($pack === null) { return array(); } $want = array_flip($pack['departments']); $present = array(); if (!function_exists('seo_public_index')) { return $pack['departments']; } $index = seo_public_index(); $byZone = isset($index['by_zone']) && is_array($index['by_zone']) ? $index['by_zone'] : array(); foreach ($want as $dept => $_) { if (!empty($byZone[$dept]) && is_array($byZone[$dept]) && count($byZone[$dept]) > 0) { $present[] = (string)$dept; } } sort($present, SORT_STRING); return $present; } } if (!function_exists('seo_geo_region_spots')) { /** * Spots publics de la region (via by_zone des depts presents). * @return array */ function seo_geo_region_spots($regionSlug) { $depts = seo_geo_region_departments_present($regionSlug); if (count($depts) === 0 || !function_exists('seo_spots_for_zone')) { return array(); } $out = array(); $seen = array(); foreach ($depts as $dept) { foreach (seo_spots_for_zone($dept) as $s) { if (!is_array($s) || empty($s['id'])) { continue; } $id = strtolower(trim((string)$s['id'])); if (isset($seen[$id])) { continue; } $seen[$id] = true; $out[] = $s; } } usort($out, function ($a, $b) { return strcasecmp( isset($a['name']) ? (string)$a['name'] : '', isset($b['name']) ? (string)$b['name'] : '' ); }); return $out; } } if (!function_exists('seo_geo_region_lead_fr')) { /** * Chapo SSR region. * @param array $spots * @param array $regionMeta * @param string[] $deptsPresent */ function seo_geo_region_lead_fr(array $spots, array $regionMeta, array $deptsPresent) { $n = count($spots); $where = isset($regionMeta['prep_phrase']) ? (string)$regionMeta['prep_phrase'] : seo_geo_region_phrase_fr(isset($regionMeta['slug']) ? $regionMeta['slug'] : ''); $sports = function_exists('seo_geo_area_sports_phrase_fr') ? seo_geo_area_sports_phrase_fr($spots) : ''; $deptPhrase = seo_geo_region_departments_list_phrase_fr($deptsPresent); $core = ($sports !== '') ? ('Découvrez les ' . $n . ' spots de ' . $sports . ' référencés ' . $where) : ('Découvrez les ' . $n . ' spots référencés ' . $where); if ($deptPhrase !== '') { $core .= ', répartis entre ' . $deptPhrase; } return $core . '. Consultez leurs conditions de pratique, leurs statistiques de fonctionnement ' . 'et les spots les plus performants selon les périodes de l\'année.'; } } if (!function_exists('seo_geo_region_departments_list_phrase_fr')) { /** « les Côtes-d'Armor, le Finistère, l'Ille-et-Vilaine et le Morbihan » */ function seo_geo_region_departments_list_phrase_fr(array $depts) { $names = array(); foreach ($depts as $d) { $lab = function_exists('seo_zone_label') ? seo_zone_label($d) : (string)$d; $short = function_exists('seo_geo_area_short_label') ? seo_geo_area_short_label($lab) : $lab; $phrase = function_exists('seo_geo_area_phrase_fr') ? seo_geo_area_phrase_fr('department', $short) : ('dans le ' . $short); /* « dans le Morbihan » ’! « le Morbihan » */ if (strpos($phrase, 'dans ') === 0) { $phrase = substr($phrase, strlen('dans ')); } $names[] = $phrase; } $n = count($names); if ($n === 0) { return ''; } if ($n === 1) { return $names[0]; } if ($n === 2) { return $names[0] . ' et ' . $names[1]; } $last = array_pop($names); return implode(', ', $names) . ' et ' . $last; } } <?php /** * Lecture / rendu SEO stats geo_region (cache region/{slug}/{sport}.json). * Agregation area uniquement. UTF-8 sans BOM. */ require_once dirname(__FILE__) . '/seo_country_stats_lib.php'; require_once dirname(__FILE__) . '/seo_geo_region.php'; require_once dirname(__FILE__) . '/seo_area_stats_lib.php'; if (!function_exists('seo_region_stats_load')) { function seo_region_stats_load($regionSlug, $sport) { try { $path = seo_geo_region_cache_path($regionSlug, $sport); if ($path === null || !is_file($path)) { return null; } $raw = @file_get_contents($path); if (!is_string($raw) || $raw === '') { return null; } $data = json_decode($raw, true); if (!is_array($data) || empty($data['region_slug']) || empty($data['sport'])) { return null; } return $data; } catch (Exception $e) { return null; } catch (Throwable $e) { return null; } } } if (!function_exists('seo_region_stats_available_sports')) { function seo_region_stats_available_sports($regionSlug) { $order = array('kite', 'wingfoil', 'windsurf', 'surf'); $out = array(); foreach ($order as $sp) { if (seo_region_stats_load($regionSlug, $sp) !== null) { $out[] = $sp; } } return $out; } } if (!function_exists('seo_region_stats_render_fr')) { function seo_region_stats_render_fr(array $regionMeta) { if (!function_exists('seo_stats_wind_sport_labels_fr')) { require_once dirname(__FILE__) . '/seo_stats_lib.php'; } if (is_file(dirname(__FILE__) . '/seo_region_stats_insights.php')) { require_once dirname(__FILE__) . '/seo_region_stats_insights.php'; } $slug = isset($regionMeta['slug']) ? (string)$regionMeta['slug'] : ''; if ($slug === '') { return ''; } $sports = seo_region_stats_available_sports($slug); if (count($sports) === 0) { return ''; } $where = isset($regionMeta['prep_phrase']) ? (string)$regionMeta['prep_phrase'] : seo_geo_region_phrase_fr($slug); $label = isset($regionMeta['label']) ? (string)$regionMeta['label'] : seo_geo_region_label($slug); $html = '<section class="seo-stats seo-country-stats seo-region-stats" aria-label="Conditions et statistiques des spots">'; $html .= '<h2 class="seo-section-title">Conditions et statistiques des spots</h2>'; $html .= '<nav class="seo-country-stats-nav" aria-label="Sports">'; foreach ($sports as $sp) { $lab = seo_stats_wind_sport_labels_fr($sp); $name = $lab ? $lab['name'] : $sp; $html .= '<a href="#seo-region-sport-' . seo_stats_h($sp) . '">' . seo_stats_h($name) . '</a>'; } $html .= '</nav>'; $opened = false; foreach ($sports as $sp) { $payload = seo_region_stats_load($slug, $sp); if ($payload === null) { continue; } $inner = seo_area_stats_render_sport_fr($payload, $label, $where, array( 'country_scope' => 'france', 'area_id' => $slug, 'label' => $label, )); if ($inner === '') { continue; } $lab = seo_stats_wind_sport_labels_fr($sp); $name = $lab ? $lab['name'] : $sp; $openAttr = !$opened ? ' open' : ''; $opened = true; $html .= '<details class="seo-stats-sport" id="seo-region-sport-' . seo_stats_h($sp) . '"' . $openAttr . '>'; $html .= '<summary class="seo-stats-sport-summary">' . seo_stats_h($name) . ' ' . seo_stats_h($where) . '</summary>'; $html .= '<div class="seo-stats-sport-body">'; $html .= '<h3 class="seo-stats-h">' . seo_stats_h($name) . ' ' . seo_stats_h($where) . ' : conditions et statistiques</h3>'; $html .= $inner; $html .= '</div></details>'; } $html .= '</section>'; return $html; } }