🗣️ Map Community Layer — Estrategia Transversal
Versión: 1.0 · Fecha: 2026-04-24
Scope: Todos los *Map apps del ecosistema (FarmaMap, FuelMap, FishMap, TravelMap, DentMap, PetMap, CannabisMap, etc.)
Objetivo: Convertir mapas-directorio en mapas-vivos donde la comunidad valida, enriquece y conversa sobre los puntos.
1. Hipótesis y por qué AHORA
| Síntoma actual | Consecuencia |
|---|---|
| Datos seed (DeepSeek/scraping) envejecen sin feedback | Pierde confianza el usuario cuando encuentra info errónea |
FishMap ya tiene /spots/suggest y catches aislados |
Reinventaremos lo mismo en cada mapa = deuda técnica |
| No hay loop de retención: usuario consulta → cierra → no vuelve | LTV bajo, sin defensibilidad vs Google Maps |
| Crowdsource mencionado en docs de petmap/camping/cannabis pero no implementado | Falta capa común que lo haga trivial activar |
Tesis: añadir interacción transforma mapas-tabla en productos sociales con efecto de red. Cada interacción mejora la calidad para todos → more users → better data → more users.
2. Las 4 primitivas de interacción (universales)
Aplicables a cualquier punto en cualquier mapa:
2.1 ⭐ Rating + Review (siempre activo)
- 1-5 estrellas + comentario opcional (max 500 chars)
- Subratings opcionales por mapa (FarmaMap: "atención", "stock"; FishMap: "accesibilidad", "pesca"; DentMap: "trato", "precio", "instalaciones")
- Helpful votes (👍 útil) → ordena reviews por utilidad
- Foto opcional (R2, max 2MB, 1 por review)
2.2 💬 Comentarios / Hilos por punto (selectivo)
- Conversación corta tipo Q&A bajo el punto ("¿está abierto sábado?")
- Owner del punto (si reclama el listing) puede responder con badge oficial
- Notificaciones a quien preguntó cuando hay respuesta
2.3 ➕ Sugerir nuevo punto (siempre activo)
- Form mínimo: nombre + lat/lng (mapa clicable o GPS) + categoría + foto + descripción
- Estado:
pending → approved/rejectedcon cola de moderación - Auto-approve si usuario tiene reputación > umbral (ver §6)
2.4 📍 Sugerir corrección (ubicación, datos, cierre) (siempre activo)
- Tipos:
move_location,update_hours,update_phone,report_closed,report_duplicate,update_data - Si N usuarios distintos reportan lo mismo en X días → auto-aplica + notifica admin
- Owner reclamado puede aceptar/rechazar instantáneo
2.5 🗨️ Foro vertical (opt-in, solo donde hay comunidad)
- Foro general del mapa (no por punto), categorías por temática
- Threads + replies, markdown ligero, fotos
- Solo se activa donde el vertical lo justifica (ver §4)
3. Arquitectura propuesta — Schema D1 polimórfico
Una sola capa, prefijo community_*, con map_id + entity_id polimórficos:
-- Tabla maestra de "qué mapas existen y qué features tienen activas"
CREATE TABLE community_maps (
map_id TEXT PRIMARY KEY, -- 'farmamap','fishmap','dentmap'...
display_name TEXT NOT NULL,
features TEXT NOT NULL DEFAULT '{}', -- JSON: {ratings:1, reviews:1, comments:1, suggest_point:1, suggest_edit:1, forum:0, photos:1}
rating_subratings TEXT, -- JSON: ["atencion","stock","precio"]
moderation_mode TEXT DEFAULT 'auto_with_threshold', -- 'manual'|'auto_with_threshold'|'trusted_only'
edit_threshold INTEGER DEFAULT 3, -- nº reportes coincidentes para auto-aplicar
created_at TEXT DEFAULT (datetime('now'))
);
-- Ratings y reviews (1 review por user+point, editable)
CREATE TABLE community_reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
map_id TEXT NOT NULL,
entity_id TEXT NOT NULL, -- id del punto en su tabla nativa
user_id INTEGER NOT NULL,
rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
subratings TEXT, -- JSON: {"atencion":5,"stock":3}
title TEXT,
body TEXT,
photo_url TEXT, -- R2 URL
helpful_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'published', -- 'published'|'hidden'|'flagged'
language TEXT, -- 'es','en','ca'... (auto-detect)
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
UNIQUE(map_id, entity_id, user_id)
);
CREATE INDEX idx_reviews_entity ON community_reviews(map_id, entity_id, status);
CREATE INDEX idx_reviews_user ON community_reviews(user_id);
-- Votos "útil" en reviews
CREATE TABLE community_review_votes (
review_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
vote INTEGER NOT NULL CHECK(vote IN (-1,1)),
created_at TEXT DEFAULT (datetime('now')),
PRIMARY KEY (review_id, user_id)
);
-- Comentarios / Q&A bajo un punto (hilos cortos)
CREATE TABLE community_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
map_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
parent_id INTEGER, -- NULL = root, else reply
user_id INTEGER NOT NULL,
body TEXT NOT NULL,
is_owner_reply INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_comments_entity ON community_comments(map_id, entity_id, status, created_at);
-- Sugerencias: nuevos puntos + ediciones
CREATE TABLE community_suggestions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
map_id TEXT NOT NULL,
entity_id TEXT, -- NULL si es nuevo punto
user_id INTEGER NOT NULL,
suggestion_type TEXT NOT NULL, -- 'new_point'|'move_location'|'update_hours'|'report_closed'|'update_data'|'report_duplicate'
payload TEXT NOT NULL, -- JSON con la propuesta
status TEXT DEFAULT 'pending', -- 'pending'|'approved'|'rejected'|'auto_applied'|'duplicate'
vote_count INTEGER DEFAULT 1, -- otros usuarios pueden +1 al mismo cambio
reviewed_by INTEGER,
reviewed_at TEXT,
reject_reason TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_suggestions_status ON community_suggestions(map_id, status, created_at);
CREATE INDEX idx_suggestions_entity ON community_suggestions(map_id, entity_id, suggestion_type);
-- Foro (opt-in por mapa)
CREATE TABLE community_forum_threads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
map_id TEXT NOT NULL,
category TEXT, -- definida en community_maps.features
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
reply_count INTEGER DEFAULT 0,
view_count INTEGER DEFAULT 0,
last_reply_at TEXT,
pinned INTEGER DEFAULT 0,
locked INTEGER DEFAULT 0,
status TEXT DEFAULT 'published',
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX idx_forum_map ON community_forum_threads(map_id, status, last_reply_at DESC);
CREATE TABLE community_forum_replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thread_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
body TEXT NOT NULL,
parent_reply_id INTEGER,
status TEXT DEFAULT 'published',
created_at TEXT DEFAULT (datetime('now'))
);
-- Reputación por usuario y mapa (gamificación + anti-spam)
CREATE TABLE community_user_reputation (
user_id INTEGER NOT NULL,
map_id TEXT NOT NULL,
score INTEGER DEFAULT 0,
reviews_count INTEGER DEFAULT 0,
suggestions_approved INTEGER DEFAULT 0,
suggestions_rejected INTEGER DEFAULT 0,
helpful_received INTEGER DEFAULT 0,
badges TEXT DEFAULT '[]', -- JSON array
trusted INTEGER DEFAULT 0, -- bypass moderación si =1
PRIMARY KEY (user_id, map_id)
);
-- Reportes de abuso (cualquier contenido)
CREATE TABLE community_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reporter_id INTEGER NOT NULL,
target_type TEXT NOT NULL, -- 'review'|'comment'|'thread'|'reply'|'suggestion'
target_id INTEGER NOT NULL,
reason TEXT NOT NULL, -- 'spam'|'offensive'|'fake'|'illegal'|'other'
details TEXT,
status TEXT DEFAULT 'pending',
created_at TEXT DEFAULT (datetime('now'))
);
-- Ownership de listings (negocios reclamados)
CREATE TABLE community_listing_owners (
map_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
user_id INTEGER NOT NULL,
verified_at TEXT,
verification_method TEXT, -- 'email_domain','phone_otp','postcard','manual'
PRIMARY KEY (map_id, entity_id, user_id)
);
Por qué polimórfico: 1 set de tablas → 30 mapas. Coste de añadir comunidad a un nuevo mapa = 1 INSERT en community_maps + UI dropdown en frontend.
4. Activación por mapa (qué tiene sentido dónde)
Leyenda: ✅ activo · 🟡 opt-in/futuro · ❌ no aplica
| Mapa | Rating | Review | Comments | Sugerir punto | Sugerir edit | Foto | Foro | Notas |
|---|---|---|---|---|---|---|---|---|
| FarmaMap | ✅ | ✅ | ✅ | 🟡 | ✅ | 🟡 | ❌ | Subratings: atención, stock, espera. Foro no, es utilitario |
| FuelMap | ✅ | 🟡 | 🟡 | ❌ | ✅ | ❌ | ❌ | Lo crítico es reportar precio (caso especial de suggest_edit) |
| FishMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Comunidad fuerte, foro por especie/zona ya tiene sentido |
| TravelMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Reviews con foto = oro. Foro de rutas/refugios |
| DentMap | ✅ | ✅ | 🟡 | ❌ | ✅ | ❌ | ❌ | Reviews críticas (Trustpilot-like). Cuidado moderación legal |
| CannabisMap | ✅ | ✅ | ✅ | 🟡 | ✅ | 🟡 | ✅ | Foro de cultivo/cepas masivo potencial |
| PetMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | Foro razas/adopciones futuro |
| GarageMap | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | Reviews son el moat (vs talleres opacos) |
| CampingMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Spots libres dependen 100% de crowdsource |
| AutocaravanMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Igual que CampingMap |
| PolitiMap | 🟡 | 🟡 | ❌ | ❌ | ❌ | ❌ | ❌ | Cuidado: alto riesgo moderación. Mejor desactivar |
| LegalMap | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | Reviews sí, comments arriesgado (consultas legales públicas) |
| HistoricalMap / TesoroMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🟡 | Crowdsource cultural natural |
| FestMap / RallyeMap / TrackMap | ✅ | ✅ | ✅ | 🟡 | ✅ | ✅ | 🟡 | Eventos con mucha conversación temporal |
| DentMap, FarmaMap, LegalMap | — | — | — | — | — | — | — | Especial: bloquear keywords médicas/legales sensibles en comments |
Regla: rating + suggest_edit = on por defecto siempre. Forum = off por defecto, on solo donde hay comunidad probada.
5. Casos especiales de "sugerir edit" por vertical
| Mapa | Edit type que importa | Auto-apply threshold |
|---|---|---|
| FuelMap | update_price (precio combustible) |
2 reportes en 1h ≥ confianza |
| FarmaMap | update_guardia (guardia hoy) |
3 reportes en 30min |
| DentMap/GarageMap | report_closed_permanent |
5 reportes en 7 días |
| FishMap | move_location + report_no_access |
3 reportes en 14 días |
| Cannabis/Pet/Camping | report_closed + update_hours |
3 reportes en 7 días |
6. Reputación, gamificación y anti-spam
Sistema de puntos (por mapa, no global)
| Acción | +Score |
|---|---|
| Review publicada | +10 |
| Review marcada útil (cada 👍) | +2 |
| Sugerencia aprobada | +25 |
| Sugerencia auto-aplicada por consenso | +15 |
| Foto añadida a punto | +5 |
| Reply en foro útil | +3 |
| Sugerencia rechazada | -5 |
| Reportado y validado spam | -50 |
Niveles + privilegios
- Nuevo (0-50): todo pasa por moderación
- Contribuidor (51-200): edits low-risk auto-apply
- Trusted (201-1000): bypass moderación, puede flag prioritario
- Mod local (1000+): panel para resolver reports de su zona
Anti-spam (capa común)
- Rate limit por IP+user: 5 reviews/h, 20 comments/h, 10 suggestions/h
- Cloudflare Turnstile en forms anónimos / usuarios nuevos
- Workers AI (
@cf/meta/llama-guard-3-8bo similar) para clasificar spam/toxicidad antes de publicar - Shadowban: contenido visible solo al autor si score < umbral negativo
- Hash de email + device fingerprint para detectar multi-cuentas
7. UX / componentes compartidos
Crear paquete storefronts/_shared/community/ (vanilla JS, drop-in):
_shared/community/
├── community-widget.js // <community-widget map-id="farmamap" entity-id="ph_123">
├── review-form.js // form de rating+review
├── comments-thread.js // hilo Q&A
├── suggest-edit-modal.js // modal "sugerir cambio"
├── suggest-point-modal.js // modal "añadir nuevo"
├── forum-list.js // lista de threads
├── reputation-badge.js // <reputation-badge user-id="...">
└── community.css // estilos consistentes
Cada bottom-sheet de cada mapa solo añade:
<community-widget map-id="fishmap" entity-id="${spot.id}"></community-widget>
Y el widget consulta /api/community/... (endpoint compartido, ver §8).
8. API unificada
Mismo Worker para todos los mapas (functions/api/community/):
GET /api/community/:map/:entity/reviews → lista paginada
POST /api/community/:map/:entity/reviews → crear review
PATCH /api/community/reviews/:id → editar (autor)
DELETE /api/community/reviews/:id → borrar (autor o mod)
POST /api/community/reviews/:id/helpful → vote útil
GET /api/community/:map/:entity/comments → hilo
POST /api/community/:map/:entity/comments → comentar / replicar
POST /api/community/:map/suggestions → nuevo punto o edit
GET /api/community/:map/suggestions?status=pending → cola moderación
POST /api/community/suggestions/:id/upvote → +1 al mismo cambio
POST /api/community/suggestions/:id/decision → admin/mod approve|reject
GET /api/community/:map/forum/threads
POST /api/community/:map/forum/threads
GET /api/community/forum/threads/:id/replies
POST /api/community/forum/threads/:id/replies
POST /api/community/reports → reportar contenido
POST /api/community/listings/:map/:entity/claim → reclamar negocio
GET /api/community/me/reputation/:map → mi score+badges
JWT compartido (el mismo de auth/google/login.js ya existente). org_id/user_id se extrae del token.
9. Moderación y panel admin
Reusar el AdminPanel existente añadiendo sección Community:
- Cola de sugerencias por mapa con acciones aprobar/rechazar
- Reports pendientes con preview del contenido
- Vista de "trending edits" (reportes con N+ votos esperando auto-apply)
- Stats: contribuidores top, reviews/día por mapa, % approval rate
Workers AI auto-mod (cron 5min):
- Toma reviews/comments/threads
publishedúltimos 5min - Llama a Llama-Guard para clasificar
- Si toxicity > 0.8 → mueve a
flagged - Si spam_score > 0.9 →
hidden+ nota al user - Si OK → nada
10. Privacidad, legal y riesgos
| Riesgo | Mitigación |
|---|---|
| Reviews difamatorias (DentMap, GarageMap) | T&C explícitos, derecho de réplica del owner reclamado, formulario LOPD para borrado |
| GDPR (datos de usuario) | user_id interno, no exponer email; export/delete account endpoint |
| Contenido sensible (CannabisMap, PolitiMap) | Disclaimers, geofencing si aplica, foro deshabilitado en PolitiMap |
| Consultas médicas/legales en comments | Bloqueo regex+LLM de comments en DentMap/FarmaMap/LegalMap (solo Q&A factual horarios/servicios) |
| Spam organizado / fake reviews | Reputación + Turnstile + LLM mod + revisión manual top-N flagged |
| Carga D1 con foros activos | Paginación obligatoria, KV cache de "trending" 5min, FTS5 índices solo donde necesario |
11. Métricas que probarán que funciona
Por mapa, dashboard semanal:
- Engagement: % visitantes únicos que interactúan (target >5%)
- Quality: avg rating, % reviews con foto, helpful_rate
- Data improvement: nº correcciones aplicadas/semana, tiempo a auto-apply
- Retention: usuarios que vuelven en 30d (interactúan vs no)
- Moderación: % flagged por LLM correcto, tiempo medio a resolver report
Norte: pasar de "consulta puntual" a "vuelvo cada semana" → multiplica LTV y SEO (UGC = páginas con contenido fresco).
12. Roadmap de implementación
Fase 1 — Fundamentos (sprint 1, 1-2 semanas)
- Migración
0001_community_layer.sqlcon todas las tablas - Worker
functions/api/community/*(CRUD reviews, suggestions) - Componente
community-widget.jsv1 (rating + review + suggest edit) - Pilot en FishMap (ya tiene la cultura crowdsource)
Fase 2 — Roll-out horizontal (sprint 2)
- Activar widget en FarmaMap, FuelMap (precio), TravelMap, DentMap, GarageMap
- AdminPanel sección Community con cola moderación
- Reputación + badges básicos
Fase 3 — Q&A + foto (sprint 3)
- Comments/Q&A por punto
- Upload foto a R2 (
community-photosbucket) + thumbs - LLM auto-mod cron + reports
Fase 4 — Foro selectivo (sprint 4)
- Foro en FishMap, TravelMap, CannabisMap, CampingMap, AutocaravanMap
- Categorías + pinning + búsqueda FTS5
- Notificaciones (email opt-in para respuestas)
Fase 5 — Listing claim + monetización (sprint 5)
- Owners reclaman negocio (verificación email/dominio)
- Owners responden reviews con badge oficial
- Tier "Verified Business" como upsell de los planes destacados existentes
13. Coste estimado
- Tablas D1: ~10MB extra/mes a velocidad media (sin problema, free tier)
- R2 fotos: 1MB avg × 100 fotos/día × 30 = 3GB/mes → ~€0.05/mes
- Workers AI mod: ~10K invocaciones/día × Llama-Guard free tier → €0
- Trabajo de desarrollo: ~4-5 sprints (igual que añadir un mapa nuevo entero)
ROI: data quality +30%, retention 30d +40%, SEO long-tail (cada review = página indexable) → debería pagar el coste el mes 2.
14. Decisión recomendada
✅ Build it, en este orden:
- Sí en todos los mapas: rating, review, sugerir edición (incluyendo "está cerrado", "movido", precio).
- Sí en mapas con comunidad activa: comments/Q&A.
- Foro solo en: FishMap, TravelMap, CannabisMap, CampingMap, AutocaravanMap (verticales con conversación recurrente).
- No tocar: PolitiMap (riesgo moderación político alto), comments en DentMap/LegalMap (riesgo legal).
- Hacerlo transversal desde el primer día (un solo schema, una sola API, un solo widget) — no replicar por mapa.
Pilot: empezar por FishMap (cultura ya existente) → validar 4 semanas → roll-out resto.