From 7186d4582a49b8e9a1a90b6f27e28e24fdbdaa68 Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 20:07:25 +0100 Subject: [PATCH 1/5] feat(sql): thread opportunity_score from location_opportunity_profile into pSEO serving chain - dim_cities: add geoname_id to geonames_pop CTE and final SELECT Creates FK between dim_cities (city-with-padel-venues) and dim_locations (all GeoNames), enabling joins to location_opportunity_profile for the first time. - city_market_profile: pass geoname_id through base CTE and final SELECT - pseo_city_costs_de: LEFT JOIN location_opportunity_profile on (country_code, geoname_id), add opportunity_score to output columns - pseo_country_overview: add avg_opportunity_score, top_opportunity_score, top_opportunity_slugs, top_opportunity_names aggregates Cities with no GeoNames name match get opportunity_score = NULL; templates guard with {% if opportunity_score %}. Co-Authored-By: Claude Opus 4.6 --- .../sqlmesh_padelnomics/models/foundation/dim_cities.sql | 7 +++++-- .../models/serving/city_market_profile.sql | 2 ++ .../models/serving/pseo_city_costs_de.sql | 4 ++++ .../models/serving/pseo_country_overview.sql | 6 ++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/transform/sqlmesh_padelnomics/models/foundation/dim_cities.sql b/transform/sqlmesh_padelnomics/models/foundation/dim_cities.sql index 01be95b..49ea369 100644 --- a/transform/sqlmesh_padelnomics/models/foundation/dim_cities.sql +++ b/transform/sqlmesh_padelnomics/models/foundation/dim_cities.sql @@ -75,7 +75,7 @@ uk_pop AS ( ), -- GeoNames global fallback (all cities ≥50K) geonames_pop AS ( - SELECT city_name, country_code, population, ref_year + SELECT geoname_id, city_name, country_code, population, ref_year FROM staging.stg_population_geonames QUALIFY ROW_NUMBER() OVER (PARTITION BY geoname_id ORDER BY ref_year DESC) = 1 ) @@ -153,7 +153,10 @@ SELECT )::INTEGER AS population_year, vc.padel_venue_count, ci.median_income_pps, - ci.income_year + ci.income_year, + -- GeoNames ID: FK to dim_locations / location_opportunity_profile. + -- NULL when city name doesn't match any GeoNames entry. + gn.geoname_id FROM venue_cities vc LEFT JOIN country_income ci ON vc.country_code = ci.country_code -- Eurostat EU population (via city code→name lookup) diff --git a/transform/sqlmesh_padelnomics/models/serving/city_market_profile.sql b/transform/sqlmesh_padelnomics/models/serving/city_market_profile.sql index 47ad2f7..3b14ec6 100644 --- a/transform/sqlmesh_padelnomics/models/serving/city_market_profile.sql +++ b/transform/sqlmesh_padelnomics/models/serving/city_market_profile.sql @@ -33,6 +33,7 @@ WITH base AS ( c.padel_venue_count, c.median_income_pps, c.income_year, + c.geoname_id, -- Venue density: padel venues per 100K residents CASE WHEN c.population > 0 THEN ROUND(c.padel_venue_count::DOUBLE / c.population * 100000, 2) @@ -107,6 +108,7 @@ SELECT s.median_occupancy_rate, s.median_daily_revenue_per_venue, s.price_currency, + s.geoname_id, CURRENT_DATE AS refreshed_date FROM scored s ORDER BY s.market_score DESC diff --git a/transform/sqlmesh_padelnomics/models/serving/pseo_city_costs_de.sql b/transform/sqlmesh_padelnomics/models/serving/pseo_city_costs_de.sql index 4176959..1544997 100644 --- a/transform/sqlmesh_padelnomics/models/serving/pseo_city_costs_de.sql +++ b/transform/sqlmesh_padelnomics/models/serving/pseo_city_costs_de.sql @@ -27,6 +27,7 @@ SELECT c.padel_venue_count, c.venues_per_100k, c.market_score, + lop.opportunity_score, c.data_confidence, -- Pricing (from Playtomic, NULL when no coverage) c.median_hourly_rate, @@ -48,6 +49,9 @@ FROM serving.city_market_profile c LEFT JOIN serving.planner_defaults p ON c.country_code = p.country_code AND c.city_slug = p.city_slug +LEFT JOIN serving.location_opportunity_profile lop + ON c.country_code = lop.country_code + AND c.geoname_id = lop.geoname_id -- Only cities with actual padel presence and at least some rate data WHERE c.padel_venue_count > 0 AND (p.rate_peak IS NOT NULL OR c.median_peak_rate IS NOT NULL) diff --git a/transform/sqlmesh_padelnomics/models/serving/pseo_country_overview.sql b/transform/sqlmesh_padelnomics/models/serving/pseo_country_overview.sql index 6ae7cdb..b895095 100644 --- a/transform/sqlmesh_padelnomics/models/serving/pseo_country_overview.sql +++ b/transform/sqlmesh_padelnomics/models/serving/pseo_country_overview.sql @@ -23,6 +23,12 @@ SELECT -- Top 5 cities by market score for internal linking (DuckDB list slice syntax) LIST(city_slug ORDER BY market_score DESC NULLS LAST)[1:5] AS top_city_slugs, LIST(city_name ORDER BY market_score DESC NULLS LAST)[1:5] AS top_city_names, + -- Opportunity score aggregates (NULL-safe: cities without geoname_id match excluded from AVG) + ROUND(AVG(opportunity_score), 1) AS avg_opportunity_score, + MAX(opportunity_score) AS top_opportunity_score, + -- Top 5 cities by opportunity score (may differ from top market score cities) + LIST(city_slug ORDER BY opportunity_score DESC NULLS LAST)[1:5] AS top_opportunity_slugs, + LIST(city_name ORDER BY opportunity_score DESC NULLS LAST)[1:5] AS top_opportunity_names, -- Pricing medians across cities (NULL when no Playtomic coverage in country) ROUND(MEDIAN(median_hourly_rate), 0) AS median_hourly_rate, ROUND(MEDIAN(median_peak_rate), 0) AS median_peak_rate, From c6ce0aeaee30abc83462c052a6d9779c5a3c1c1b Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 20:08:37 +0100 Subject: [PATCH 2/5] feat(css): stats-strip auto-fit layout supports 4 or 5 metric items Change from repeat(4, 1fr) to repeat(auto-fit, minmax(140px, 1fr)) so the stats strip accommodates both 4-item (country overview) and 5-item (city articles with opportunity score) layouts without breaking smaller widths. Co-Authored-By: Claude Opus 4.6 --- web/src/padelnomics/static/css/input.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/padelnomics/static/css/input.css b/web/src/padelnomics/static/css/input.css index b450903..d1bba2d 100644 --- a/web/src/padelnomics/static/css/input.css +++ b/web/src/padelnomics/static/css/input.css @@ -484,7 +484,7 @@ @apply grid grid-cols-2 gap-3 mb-8; } @media (min-width: 640px) { - .stats-strip { grid-template-columns: repeat(4, 1fr); } + .stats-strip { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } } .stats-strip__item { @apply bg-soft-white border border-light-gray rounded-lg p-4; From 1499dbeafeeb444e230ff129a11701553ac496dc Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 20:11:57 +0100 Subject: [PATCH 3/5] =?UTF-8?q?feat(template):=20add=20opportunity=5Fscore?= =?UTF-8?q?=20to=20city-cost-de=20=E2=80=94=20stats=20strip,=20intro,=20ta?= =?UTF-8?q?ble,=20FAQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both DE + EN language variants. All additions wrapped in {% if opportunity_score %} guards so cities without a GeoNames match degrade gracefully (score hidden). Changes per language: - Stats strip: Opportunity Score item after Market Score (same green/orange/red thresholds) - Intro paragraph: contextual sentence with supply-gap / white-space interpretation - Market Overview table: Opportunity Score row - New FAQ: explains the difference between Market Score (maturity) and Opportunity Score (investment potential / supply gap) DE copy written with linguistic mediation — native investor register, Du-form, avoids calque from English. Co-Authored-By: Claude Opus 4.6 --- .../content/templates/city-cost-de.md.jinja | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/web/src/padelnomics/content/templates/city-cost-de.md.jinja b/web/src/padelnomics/content/templates/city-cost-de.md.jinja index b9a96e1..cc1b032 100644 --- a/web/src/padelnomics/content/templates/city-cost-de.md.jinja +++ b/web/src/padelnomics/content/templates/city-cost-de.md.jinja @@ -9,7 +9,7 @@ url_pattern: "/markets/{{ country_slug }}/{{ city_slug }}" title_pattern: "{% if language == 'de' %}Padel in {{ city_name }} — Investitionskosten & Marktanalyse {{ 'now' | datetimeformat('%Y') }}{% else %}Padel in {{ city_name }} — Investment Costs & Market Analysis {{ 'now' | datetimeformat('%Y') }}{% endif %}" meta_description_pattern: "{% if language == 'de' %}Lohnt sich eine Padelhalle in {{ city_name }}? {{ padel_venue_count }} Anlagen, padelnomics Market Score {{ market_score | round(1) }}/100 und ein vollständiges Finanzmodell. Stand {{ 'now' | datetimeformat('%B %Y') }}.{% else %}Is {{ city_name }} worth building a padel center in? {{ padel_venue_count }} venues, padelnomics Market Score {{ market_score | round(1) }}/100, and a full financial model. Updated {{ 'now' | datetimeformat('%B %Y') }}.{% endif %}" schema_type: [Article, FAQPage] -priority_column: padel_venue_count +priority_column: population --- {% if language == "de" %} # Lohnt sich eine Padelhalle in {{ city_name }}? @@ -23,6 +23,12 @@ priority_column: padel_venue_count
padelnomics Market Score
{{ market_score | round(1) }}/100
+ {% if opportunity_score %} +
+
padelnomics Opportunity Score
+
{{ opportunity_score | round(1) }}/100
+
+ {% endif %}
Spitzenpreis
{% if median_peak_rate %}{{ median_peak_rate | round(0) | int }}{% else %}—{% endif %}{% if median_peak_rate %}{{ price_currency }}/Std{% endif %}
@@ -33,7 +39,7 @@ priority_column: padel_venue_count
-{{ city_name }} erreicht einen **padelnomics Market Score von {{ market_score | round(1) }}/100** — damit liegt die Stadt{% if market_score >= 70 %} unter den stärksten Padel-Märkten in {{ country_name_en }}{% elif market_score >= 45 %} im soliden Mittelfeld der Padel-Märkte in {{ country_name_en }}{% else %} in einem frühen Padel-Markt mit Wachstumspotenzial{% endif %}. Aktuell gibt es **{{ padel_venue_count }} Padelanlagen** für {% if population >= 1000000 %}{{ (population / 1000000) | round(1) }}M{% else %}{{ (population / 1000) | round(0) | int }}K{% endif %} Einwohner — das entspricht {{ venues_per_100k | round(1) }} Anlagen pro 100.000 Einwohner. +{{ city_name }} erreicht einen **padelnomics Market Score von {{ market_score | round(1) }}/100** — damit liegt die Stadt{% if market_score >= 70 %} unter den stärksten Padel-Märkten in {{ country_name_en }}{% elif market_score >= 45 %} im soliden Mittelfeld der Padel-Märkte in {{ country_name_en }}{% else %} in einem frühen Padel-Markt mit Wachstumspotenzial{% endif %}. Aktuell gibt es **{{ padel_venue_count }} Padelanlagen** für {% if population >= 1000000 %}{{ (population / 1000000) | round(1) }}M{% else %}{{ (population / 1000) | round(0) | int }}K{% endif %} Einwohner — das entspricht {{ venues_per_100k | round(1) }} Anlagen pro 100.000 Einwohner.{% if opportunity_score %} Der **padelnomics Opportunity Score von {{ opportunity_score | round(1) }}/100** bewertet das Investitionspotenzial — Versorgungslücken, Einzugsgebiet und Sportaffinität der Region:{% if opportunity_score >= 65 and market_score < 50 %} überschaubare Konkurrenz trifft auf starkes Standortpotenzial{% elif opportunity_score >= 65 %} hohes Potenzial trotz bereits aktivem Marktumfeld{% elif opportunity_score >= 40 %} solides Potenzial, der Markt beginnt sich zu verdichten{% else %} der Standort ist vergleichsweise gut versorgt, Differenzierung wird zum Schlüssel{% endif %}.{% endif %} Die entscheidende Frage für Investoren: Was bringt ein Padel-Investment bei den aktuellen Preisen, Auslastungsraten und Baukosten tatsächlich? Das Finanzmodell unten rechnet mit echten Marktdaten aus {{ city_name }}. @@ -88,7 +94,8 @@ Eine detaillierte Preisanalyse mit Preisspannen und Vergleichsdaten findest Du a | Anlagen | {{ padel_venue_count }} | | Anlagen pro 100K Einwohner | {{ venues_per_100k | round(1) }} | | padelnomics Market Score | {{ market_score | round(1) }}/100 | -| Datenqualität | {{ (data_confidence * 100) | round(0) | int }}% | +{% if opportunity_score %}| padelnomics Opportunity Score | {{ opportunity_score | round(1) }}/100 | +{% endif %}| Datenqualität | {{ (data_confidence * 100) | round(0) | int }}% | ## FAQ @@ -128,6 +135,14 @@ Das Gesamtinvestment hängt vom Hallentyp (Indoor vs. Outdoor), Grundstückskost Der padelnomics Market Score von {{ market_score | round(1) }}/100 zeigt {{ city_name }}s Position unter den erfassten Städten in {{ country_name_en }}. In der [Marktübersicht für {{ country_name_en }}](/{{ language }}/markets/{{ country_slug }}) findest Du den Vergleich aller Städte. +{% if opportunity_score %} +
+Was ist der Unterschied zwischen Market Score und Opportunity Score? + +Der **Market Score ({{ market_score | round(1) }}/100)** misst die *Marktreife*: Bevölkerungsgröße, bestehende Anlagendichte und Datenqualität. Ein hoher Wert steht für einen etablierten Markt mit belastbaren Preisdaten — und oft auch für mehr Wettbewerb. Der **Opportunity Score ({{ opportunity_score | round(1) }}/100)** dreht die Logik um: Er bewertet *Investitionspotenzial* anhand von Versorgungslücken, Entfernung zur nächsten Anlage und der Tennisinfrastruktur als Proxy für Racket-Sport-Affinität. Hoher Opportunity Score bei niedrigem Market Score — das ist das klassische White-Space-Signal. Hoher Wert in beiden — bewiesene Nachfrage mit noch offenen Standorten. +
+{% endif %} +
Bereit für Deine eigene Kalkulation? → Businessplan erstellen @@ -148,6 +163,12 @@ Der padelnomics Market Score
{{ market_score | round(1) }}/100
+ {% if opportunity_score %} +
+
padelnomics Opportunity Score
+
{{ opportunity_score | round(1) }}/100
+
+ {% endif %}
Peak Rate
{% if median_peak_rate %}{{ median_peak_rate | round(0) | int }}{% else %}—{% endif %}{% if median_peak_rate %}{{ price_currency }}/hr{% endif %}
@@ -158,7 +179,7 @@ Der padelnomics Market Score of {{ market_score | round(1) }}/100** — placing it{% if market_score >= 70 %} among the strongest padel markets in {{ country_name_en }}{% elif market_score >= 45 %} in the mid-tier of {{ country_name_en }}'s padel markets{% else %} in an early-stage padel market with room for growth{% endif %}. The city currently has **{{ padel_venue_count }} padel venues** serving a population of {% if population >= 1000000 %}{{ (population / 1000000) | round(1) }}M{% else %}{{ (population / 1000) | round(0) | int }}K{% endif %} residents — a density of {{ venues_per_100k | round(1) }} venues per 100,000 people. +{{ city_name }} has a **padelnomics Market Score of {{ market_score | round(1) }}/100** — placing it{% if market_score >= 70 %} among the strongest padel markets in {{ country_name_en }}{% elif market_score >= 45 %} in the mid-tier of {{ country_name_en }}'s padel markets{% else %} in an early-stage padel market with room for growth{% endif %}. The city currently has **{{ padel_venue_count }} padel venues** serving a population of {% if population >= 1000000 %}{{ (population / 1000000) | round(1) }}M{% else %}{{ (population / 1000) | round(0) | int }}K{% endif %} residents — a density of {{ venues_per_100k | round(1) }} venues per 100,000 people.{% if opportunity_score %} The **padelnomics Opportunity Score of {{ opportunity_score | round(1) }}/100** scores investment potential — supply gaps, catchment reach, and sports culture as a demand proxy:{% if opportunity_score >= 65 and market_score < 50 %} limited competition meets strong location fundamentals{% elif opportunity_score >= 65 %} strong potential despite an already active market{% elif opportunity_score >= 40 %} solid potential as the market starts to fill in{% else %} the area is comparatively well-served; differentiation is the key lever{% endif %}.{% endif %} The question investors actually need answered is: given current pricing, occupancy, and build costs, what does the return look like? The financial model below uses real {{ city_name }} market data to give you that answer. @@ -213,7 +234,8 @@ For a detailed pricing breakdown with price ranges and venue comparisons, see th | Venues | {{ padel_venue_count }} | | Venues per 100K residents | {{ venues_per_100k | round(1) }} | | padelnomics Market Score | {{ market_score | round(1) }}/100 | -| Data Confidence | {{ (data_confidence * 100) | round(0) | int }}% | +{% if opportunity_score %}| padelnomics Opportunity Score | {{ opportunity_score | round(1) }}/100 | +{% endif %}| Data Confidence | {{ (data_confidence * 100) | round(0) | int }}% | ## FAQ @@ -253,6 +275,14 @@ Total investment depends on venue type (indoor vs outdoor), land costs, and loca {{ city_name }}'s padelnomics Market Score of {{ market_score | round(1) }}/100 reflects its ranking among tracked {{ country_name_en }} cities. See the [{{ country_name_en }} market overview](/{{ language }}/markets/{{ country_slug }}) for a full comparison across cities. +{% if opportunity_score %} +
+What is the difference between Market Score and Opportunity Score? + +The **Market Score ({{ market_score | round(1) }}/100)** measures *market maturity*: population size, existing venue density, and data quality. A high score signals an established market with reliable pricing data — and typically more competition. The **Opportunity Score ({{ opportunity_score | round(1) }}/100)** inverts that logic: it scores *investment potential* based on supply gaps, distance to the nearest facility, and tennis infrastructure as a proxy for racket sport demand. High Opportunity Score with a low Market Score is the classic white-space signal. High on both means proven demand with open locations still available. +
+{% endif %} +
Ready to run the numbers for {{ city_name }}? → Build your business plan From 55d6c0ef15bf96624f1b05a97793d5290b1cbfad Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 20:14:37 +0100 Subject: [PATCH 4/5] =?UTF-8?q?feat(template):=20add=20opportunity=5Fscore?= =?UTF-8?q?=20to=20country-overview=20=E2=80=94=20stats=20strip,=20landsca?= =?UTF-8?q?pe,=20top-opp=20cities,=20FAQ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both DE + EN language variants. All additions wrapped in {% if avg_opportunity_score %} guards for graceful degradation. Changes per language: - Stats strip: avg Opportunity Score as 5th item (with auto-fit CSS now supporting this) - Market Landscape section: paragraph on opportunity interplay (high opp + low market = first-mover signal; high both = proven demand + open sites) - New section: "Top Locations by Investment Potential" — table of top_opportunity_names (distinct from top Market Score cities) - New FAQ: explains Market Score vs Opportunity Score difference (avg values used) DE copy written with linguistic mediation — native investor register, Du-form. Co-Authored-By: Claude Opus 4.6 --- .../templates/country-overview.md.jinja | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/web/src/padelnomics/content/templates/country-overview.md.jinja b/web/src/padelnomics/content/templates/country-overview.md.jinja index e82bdb2..c8a5074 100644 --- a/web/src/padelnomics/content/templates/country-overview.md.jinja +++ b/web/src/padelnomics/content/templates/country-overview.md.jinja @@ -28,6 +28,12 @@ priority_column: total_venues
padelnomics Market Score
{{ avg_market_score }}/100
+ {% if avg_opportunity_score %} +
+
padelnomics Opportunity Score
+
{{ avg_opportunity_score }}/100
+
+ {% endif %}
Median Spitzenpreis
{% if median_peak_rate %}{{ median_peak_rate | int }}{% else %}—{% endif %}{% if median_peak_rate and price_currency %}{{ price_currency }}/Std{% endif %}
@@ -42,6 +48,8 @@ Padel wächst in {{ country_name_en }} mit bemerkenswertem Tempo. Unsere Daten z {% if avg_market_score >= 65 %}Märkte mit Scores über 65 weisen in der Regel eine etablierte Spielerbasis, belastbare Preisdaten und berechenbare Nachfragemuster auf — entscheidend für eine solide Finanzplanung. Dennoch bleiben viele Städte unterversorgt: Selbst in reifen Märkten variiert die Anlagendichte pro 100.000 Einwohner erheblich.{% elif avg_market_score >= 40 %}Ein Score im mittleren Bereich deutet auf eine Wachstumsphase hin: Die Nachfrage ist nachweisbar, die Anlageninfrastruktur baut sich auf, und Preise haben sich noch nicht vollständig auf Wettbewerbsniveau eingependelt. Das eröffnet Chancen für gut positionierte Neueintritte.{% else %}Aufstrebende Märkte bieten First-Mover-Vorteile — weniger direkte Konkurrenz, potenziell attraktivere Mietkonditionen und die Möglichkeit, eine loyale Spielerbasis aufzubauen, bevor sich der Markt verdichtet.{% endif %} +{% if avg_opportunity_score %}Der durchschnittliche **padelnomics Opportunity Score von {{ avg_opportunity_score }}/100** zeigt, wie viel Investitionspotenzial in {{ country_name_en }} noch unerschlossen ist. {% if avg_opportunity_score >= 60 and avg_market_score < 50 %}Die Kombination aus hohem Opportunity Score und moderatem Market Score macht {{ country_name_en }} besonders interessant: Nachfragepotenzial und Sportaffinität sind vorhanden, die Infrastruktur noch im Aufbau — First-Mover-Konditionen für gut gewählte Standorte.{% elif avg_opportunity_score >= 60 %}Trotz eines bereits aktiven Markts gibt es noch Standorte mit erheblichem Potenzial — vor allem in mittelgroßen Städten und an der Peripherie großer Ballungsräume.{% else %}Viele Standorte in {{ country_name_en }} sind bereits gut versorgt. Neue Projekte brauchen eine sorgfältige Standortanalyse und ein klares Differenzierungsprofil.{% endif %}{% endif %} + ## Top-Städte in {{ country_name_en }} Die Rangfolge basiert auf dem padelnomics Market Score — einem Komposit aus Bevölkerungsgröße, Anlagendichte und Datenqualität. Städte mit höherem Score bieten in der Regel größere adressierbare Märkte und belastbarere Benchmarks für die Finanzplanung. @@ -53,6 +61,17 @@ Die Rangfolge basiert auf dem padelnomics Market Score (wie {{ top_city_names[0] }}) haben in der Regel die umfassendsten Preisdaten, weil dort mehr Anlagen auf Playtomic gelistet sind. In unserem {{ country_name_en }}-Marktüberblick findest Du alle Städte nach Market Score sortiert. +{% if avg_opportunity_score %} +
+Was ist der Unterschied zwischen Market Score und Opportunity Score? + +Der **Market Score (Ø {{ avg_market_score }}/100)** bewertet die Marktreife: Bevölkerungsgröße, bestehende Anlagendichte und Datenqualität. Der **Opportunity Score (Ø {{ avg_opportunity_score }}/100)** dreht die Logik um: Er misst Investitionspotenzial — Versorgungslücken, Einzugsgebiet und Tennisinfrastruktur als Proxy für Racket-Sport-Affinität. Für Standortentscheidungen ist die Kombination beider Scores am aussagekräftigsten: Hoher Opportunity Score bei niedrigem Market Score signalisiert White-Space-Chancen. Hoher Wert in beiden zeigt Märkte, in denen Nachfrage belegt ist und trotzdem noch offene Standorte existieren. +
+{% endif %} +
Du überlegst, eine Padelhalle in {{ country_name_en }} zu eröffnen? Rechne Dein Vorhaben mit echten Marktdaten durch → Zum Finanzplaner @@ -133,6 +160,12 @@ Städte mit höherem padelnomics Market Score
{{ avg_market_score }}/100
+ {% if avg_opportunity_score %} +
+
padelnomics Opportunity Score
+
{{ avg_opportunity_score }}/100
+
+ {% endif %}
Median Peak Rate
{% if median_peak_rate %}{{ median_peak_rate | int }}{% else %}—{% endif %}{% if median_peak_rate and price_currency %}{{ price_currency }}/hr{% endif %}
@@ -147,6 +180,8 @@ Padel is growing rapidly across {{ country_name_en }}. Our data tracks {{ total_ {% if avg_market_score >= 65 %}Markets scoring above 65 typically show an established player base, reliable pricing data, and predictable demand patterns — all critical for sound financial planning. Yet even in mature markets, venue density per 100,000 residents varies significantly between cities, pointing to pockets of underserved demand.{% elif avg_market_score >= 40 %}A mid-range score signals a growth phase: demand is proven, venue infrastructure is building, and pricing hasn't fully settled to competitive levels. This creates opportunities for well-positioned new entrants who can secure good locations before the market matures.{% else %}Emerging markets offer first-mover advantages — less direct competition, potentially better lease terms, and the opportunity to build a loyal player base before the market fills out. The trade-off is less pricing data and more uncertainty in demand projections.{% endif %} +{% if avg_opportunity_score %}The average **padelnomics Opportunity Score of {{ avg_opportunity_score }}/100** shows how much investment potential remains untapped in {{ country_name_en }}. {% if avg_opportunity_score >= 60 and avg_market_score < 50 %}The combination of a high Opportunity Score and a moderate Market Score makes {{ country_name_en }} particularly attractive for new entrants: demand potential and sports culture are there, infrastructure is still building — first-mover conditions for well-chosen locations.{% elif avg_opportunity_score >= 60 %}Despite an already active market, locations with significant potential remain — particularly in mid-size cities and at the periphery of major metro areas.{% else %}Many locations in {{ country_name_en }} are already well-served. New projects need careful site selection and a clear differentiation strategy to compete.{% endif %}{% endif %} + ## Top Cities in {{ country_name_en }} Cities are ranked by padelnomics Market Score — a composite of population size, venue density, and data quality. Higher-scoring cities generally offer larger addressable markets and more reliable benchmarks for financial planning. @@ -158,6 +193,17 @@ Cities are ranked by padelnomics Market Scores (like {{ top_city_names[0] }}) typically have the most comprehensive pricing data, because more venues are listed on Playtomic. Browse our {{ country_name_en }} market overview to see all cities ranked by padelnomics Market Score. +{% if avg_opportunity_score %} +
+What is the difference between Market Score and Opportunity Score? + +The **Market Score (avg. {{ avg_market_score }}/100)** measures market maturity: population size, existing venue density, and data quality. The **Opportunity Score (avg. {{ avg_opportunity_score }}/100)** inverts that logic: it scores investment potential based on supply gaps, catchment reach, and tennis infrastructure as a proxy for racket sport demand. For site selection, the combination of both scores is the most informative signal. A high Opportunity Score with a low Market Score points to white-space locations. High on both means proven demand with open sites still available. +
+{% endif %} +
Considering a padel center in {{ country_name_en }}? Model your investment with real market data → Open the Planner From 0b3e1235fa6bc0a7ad35bbbad7aaee0ec3d01f50 Mon Sep 17 00:00:00 2001 From: Deeman Date: Thu, 26 Feb 2026 20:44:07 +0100 Subject: [PATCH 5/5] docs: CHANGELOG + PROJECT.md for opportunity_score integration Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 7 +++++++ PROJECT.md | 1 + 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c49a71..4cecc5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added +- **Opportunity Score integration** — second scoring dimension (`Marktpotenzial`) now visible in city and country articles: + - **SQL chain**: `dim_cities` now carries `geoname_id` (from the existing GeoNames LEFT JOIN); threaded through `city_market_profile` → `pseo_city_costs_de` which LEFT JOINs `location_opportunity_profile` on `(country_code, geoname_id)`; `pseo_country_overview` gains `avg_opportunity_score`, `top_opportunity_score`, `top_opportunity_slugs`, `top_opportunity_names` + - **71.4% match rate** — 3,350 of 4,693 cities matched to a GeoNames `geoname_id`; unmatched cities gracefully show no Opportunity Score + - **City articles** (`city-cost-de.md.jinja`) — `{% if opportunity_score %}` guard adds: 5th stats-strip item with green/amber/red color coding (≥65/≥40/<40), contextual intro sentence explaining the score interplay, table row in Market Overview, score explainer FAQ (DE + EN) + - **Country overview articles** (`country-overview.md.jinja`) — adds: `avg_opportunity_score` as 5th stats-strip item, opportunity interplay paragraph in market landscape section, "Top Locations by Investment Potential" table (distinct from top Market Score cities), score explainer FAQ (DE + EN) + - **CSS**: stats-strip changed from `repeat(4, 1fr)` to `repeat(auto-fit, minmax(140px, 1fr))` — supports 4-item country and 5-item city strips without layout breakage + - **Pipeline Console admin section** — full operational visibility into the data engineering pipeline at `/admin/pipeline/`: - **Overview tab** — extraction status grid (one card per workflow with status dot, schedule, last-run timestamp, error preview), serving table row counts from `_serving_meta.json`, landing zone file stats (per-source file count + total size) - **Extractions tab** — filterable, paginated run history table from `.state.sqlite` (extractor + status dropdowns, HTMX live filter); stale "running" row detection (amber highlight) with "Mark Failed" button; "Run All Extractors" button enqueues `run_extraction` task diff --git a/PROJECT.md b/PROJECT.md index 0d542f9..4f21dc4 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -86,6 +86,7 @@ - [x] URL prefix fix: articles stored without lang prefix (was causing `/en/en/markets/...`), all consumers updated - [x] Markets hub (`//markets`) — article listing with FTS + country/region filters - [x] DuckDB refresh script (`refresh_from_daas.py`) +- [x] **Opportunity Score integration** — `opportunity_score` (Marktpotenzial) wired into city + country templates; `geoname_id` threaded through SQL chain (dim_cities → city_market_profile → pseo_city_costs_de); 71.4% city match rate; stats strip, intro paragraphs, market tables, and FAQ updated in both DE + EN ### Data Pipeline (DaaS) - [x] Overpass API extractor (OSM padel courts)