Browse Source

first commit

master
Yutsuo 2 months ago
commit
ab3352f765
  1. 1
      .gitignore
  2. 247
      .venv/bin/Activate.ps1
  3. 70
      .venv/bin/activate
  4. 27
      .venv/bin/activate.csh
  5. 69
      .venv/bin/activate.fish
  6. 8
      .venv/bin/httpx
  7. 8
      .venv/bin/idna
  8. 8
      .venv/bin/pip
  9. 8
      .venv/bin/pip3
  10. 8
      .venv/bin/pip3.12
  11. 8
      .venv/bin/playwright
  12. 164
      .venv/include/site/python3.12/greenlet/greenlet.h
  13. 5
      .venv/pyvenv.cfg
  14. 191
      AGENTS.md
  15. 216
      README.md
  16. BIN
      __pycache__/dfimoveis_scraper.cpython-312.pyc
  17. BIN
      __pycache__/dfimoveis_scraper.cpython-314.pyc
  18. 1215
      dfimoveis_scraper.py
  19. 238
      docs/ACTUAL_SCHEMA.md
  20. 542
      docs/ANALYSIS_RULES.md
  21. 5
      docs/CHAT_HANDOFF.md
  22. 481
      docs/DATA_MODEL.md
  23. 265
      docs/PROJECT_CONTEXT.md
  24. 438
      docs/PROPERTY_CRITERIA.md
  25. 22
      headers.txt
  26. 8
      house-quest.code-workspace
  27. 172
      reports/cruzeiro_top10/README.md
  28. BIN
      reports/cruzeiro_top10/__pycache__/analysis.cpython-314.pyc
  29. BIN
      reports/cruzeiro_top10/__pycache__/photo_similarity.cpython-314.pyc
  30. 133
      reports/cruzeiro_top10/analysis.py
  31. 22
      reports/cruzeiro_top10/parameters.json
  32. 87
      reports/cruzeiro_top10/photo_similarity.py
  33. 48
      reports/cruzeiro_top10/query.sql
  34. 11
      reports/cruzeiro_top10/results.csv
  35. 133
      reports/cruzeiro_vs_mangueiral/README.md
  36. 31
      reports/cruzeiro_vs_mangueiral/parameters.json
  37. 21
      reports/cruzeiro_vs_mangueiral/results.csv
  38. 303
      reports/database_assessment.md
  39. 184
      reports/mangueiral_top10/README.md
  40. BIN
      reports/mangueiral_top10/__pycache__/analysis.cpython-312.pyc
  41. BIN
      reports/mangueiral_top10/__pycache__/analysis.cpython-314.pyc
  42. BIN
      reports/mangueiral_top10/__pycache__/photo_similarity.cpython-312.pyc
  43. BIN
      reports/mangueiral_top10/__pycache__/photo_similarity.cpython-314.pyc
  44. 130
      reports/mangueiral_top10/analysis.py
  45. 29
      reports/mangueiral_top10/parameters.json
  46. 87
      reports/mangueiral_top10/photo_similarity.py
  47. 65
      reports/mangueiral_top10/query.sql
  48. 11
      reports/mangueiral_top10/results.csv
  49. 9
      requirements-dfimoveis.txt
  50. BIN
      resources/maps/00aecaae-3e6b-4605-958f-70dbdb2b24b8.png
  51. BIN
      resources/maps/1d424088-057c-43e8-9459-306978d0a456.png
  52. BIN
      resources/maps/643d59ab-9deb-48b8-8b8f-f3d444aab4a2.png
  53. BIN
      resources/maps/6f86274e-fb86-4d74-b526-fe8fc0f34e07.png
  54. BIN
      resources/maps/7c219f0a-933e-4643-9c50-c83081a75677.png
  55. BIN
      resources/maps/photo_2026-07-16_17-17-11.jpg
  56. BIN
      resources/maps/photo_2026-07-16_17-17-37.jpg
  57. BIN
      resources/maps/photo_2026-07-16_17-17-44.jpg
  58. BIN
      resources/maps/photo_2026-07-16_17-21-28.jpg
  59. BIN
      resources/maps/photo_2026-07-16_17-21-32.jpg
  60. BIN
      resources/maps/photo_2026-07-16_17-21-35.jpg
  61. BIN
      resources/plans/74920737-ED7F-4E08-A7C9-C8F3C92C598A.jpeg
  62. BIN
      resources/plans/B65F8041-DA75-4982-A5B5-15BB2B8AB465.jpeg
  63. 1
      response.html
  64. 3
      search_list.txt
  65. 19
      used_commands.ps1

1
.gitignore vendored

@ -0,0 +1 @@
dfimoveis_data

247
.venv/bin/Activate.ps1 vendored

@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
.venv/bin/activate vendored

@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/f2256342/forge/webscraper/.venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/f2256342/forge/webscraper/.venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(.venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(.venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

27
.venv/bin/activate.csh vendored

@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/f2256342/forge/webscraper/.venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(.venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(.venv) '
endif
alias pydoc python -m pydoc
rehash

69
.venv/bin/activate.fish vendored

@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/f2256342/forge/webscraper/.venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(.venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(.venv) '
end

8
.venv/bin/httpx vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
.venv/bin/idna vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from idna.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
.venv/bin/pip vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
.venv/bin/pip3 vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
.venv/bin/pip3.12 vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
.venv/bin/playwright vendored

@ -0,0 +1,8 @@
#!/home/f2256342/forge/webscraper/.venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from playwright.__main__ import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

164
.venv/include/site/python3.12/greenlet/greenlet.h vendored

@ -0,0 +1,164 @@
/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */
/* Greenlet object interface */
#ifndef Py_GREENLETOBJECT_H
#define Py_GREENLETOBJECT_H
#include <Python.h>
#ifdef __cplusplus
extern "C" {
#endif
/* This is deprecated and undocumented. It does not change. */
#define GREENLET_VERSION "1.0.0"
#ifndef GREENLET_MODULE
#define implementation_ptr_t void*
#endif
typedef struct _greenlet {
PyObject_HEAD
PyObject* weakreflist;
PyObject* dict;
implementation_ptr_t pimpl;
} PyGreenlet;
#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type))
/* C API functions */
/* Total number of symbols that are exported */
#define PyGreenlet_API_pointers 12
#define PyGreenlet_Type_NUM 0
#define PyExc_GreenletError_NUM 1
#define PyExc_GreenletExit_NUM 2
#define PyGreenlet_New_NUM 3
#define PyGreenlet_GetCurrent_NUM 4
#define PyGreenlet_Throw_NUM 5
#define PyGreenlet_Switch_NUM 6
#define PyGreenlet_SetParent_NUM 7
#define PyGreenlet_MAIN_NUM 8
#define PyGreenlet_STARTED_NUM 9
#define PyGreenlet_ACTIVE_NUM 10
#define PyGreenlet_GET_PARENT_NUM 11
#ifndef GREENLET_MODULE
/* This section is used by modules that uses the greenlet C API */
static void** _PyGreenlet_API = NULL;
# define PyGreenlet_Type \
(*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM])
# define PyExc_GreenletError \
((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM])
# define PyExc_GreenletExit \
((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM])
/*
* PyGreenlet_New(PyObject *args)
*
* greenlet.greenlet(run, parent=None)
*/
# define PyGreenlet_New \
(*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \
_PyGreenlet_API[PyGreenlet_New_NUM])
/*
* PyGreenlet_GetCurrent(void)
*
* greenlet.getcurrent()
*/
# define PyGreenlet_GetCurrent \
(*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM])
/*
* PyGreenlet_Throw(
* PyGreenlet *greenlet,
* PyObject *typ,
* PyObject *val,
* PyObject *tb)
*
* g.throw(...)
*/
# define PyGreenlet_Throw \
(*(PyObject * (*)(PyGreenlet * self, \
PyObject * typ, \
PyObject * val, \
PyObject * tb)) \
_PyGreenlet_API[PyGreenlet_Throw_NUM])
/*
* PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args)
*
* g.switch(*args, **kwargs)
*/
# define PyGreenlet_Switch \
(*(PyObject * \
(*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \
_PyGreenlet_API[PyGreenlet_Switch_NUM])
/*
* PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent)
*
* g.parent = new_parent
*/
# define PyGreenlet_SetParent \
(*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \
_PyGreenlet_API[PyGreenlet_SetParent_NUM])
/*
* PyGreenlet_GetParent(PyObject* greenlet)
*
* return greenlet.parent;
*
* This could return NULL even if there is no exception active.
* If it does not return NULL, you are responsible for decrementing the
* reference count.
*/
# define PyGreenlet_GetParent \
(*(PyGreenlet* (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_GET_PARENT_NUM])
/*
* deprecated, undocumented alias.
*/
# define PyGreenlet_GET_PARENT PyGreenlet_GetParent
# define PyGreenlet_MAIN \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_MAIN_NUM])
# define PyGreenlet_STARTED \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_STARTED_NUM])
# define PyGreenlet_ACTIVE \
(*(int (*)(PyGreenlet*)) \
_PyGreenlet_API[PyGreenlet_ACTIVE_NUM])
/* Macro that imports greenlet and initializes C API */
/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we
keep the older definition to be sure older code that might have a copy of
the header still works. */
# define PyGreenlet_Import() \
{ \
_PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \
}
#endif /* GREENLET_MODULE */
#ifdef __cplusplus
}
#endif
#endif /* !Py_GREENLETOBJECT_H */

5
.venv/pyvenv.cfg vendored

@ -0,0 +1,5 @@
home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /home/f2256342/forge/webscraper/.venv

191
AGENTS.md

@ -0,0 +1,191 @@
# AGENTS.md — Busca de Imóveis DF
## Finalidade
Este repositório apoia a coleta, organização e análise histórica de anúncios imobiliários do Distrito Federal para uma decisão residencial de longo prazo.
Antes de trabalhar no projeto, leia integralmente:
- `docs/PROJECT_CONTEXT.md`
- `docs/PROPERTY_CRITERIA.md`
- `docs/DATA_MODEL.md`
- `docs/ANALYSIS_RULES.md`
Esses documentos são a referência operacional do projeto. Quando houver conflito:
1. uma instrução explícita e recente do usuário prevalece;
2. depois, prevalecem estes documentos;
3. por último, hipóteses inferidas a partir dos dados.
Nunca trate uma hipótese como preferência confirmada.
Para auxiliar no entendimento e contexto, as plantas das casas dos Jardins Mangueiral estão em:
- `resources/plans`
Para auxiliar no entendimento e contexto, foram feitas capturas dos mapas dos Jardins Mangueiral, que foram salvas na em:
- `resources/maps`
---
## Banco de dados
O banco principal deve ser tratado como **estritamente somente leitura**.
### Regras obrigatórias
- Nunca execute `INSERT`, `UPDATE`, `DELETE`, `DROP`, `ALTER`, `CREATE`, `REPLACE`, `VACUUM`, `REINDEX`, `ATTACH` ou comandos equivalentes contra o banco original.
- Não substitua, mova, compacte, migre ou renomeie o arquivo original.
- Não remova arquivos `-wal` ou `-shm` sem uma instrução explícita.
- Não execute scripts de scraping contra o banco durante uma análise sem autorização.
- Use `LIMIT` em consultas exploratórias.
- Não imprima tabelas inteiras no terminal.
- Prefira agregações, amostras pequenas e exportações derivadas.
- Salve resultados em `reports/`, `exports/` ou `data/derived/`.
- Nunca grave resultados derivados dentro do banco original.
- Antes de qualquer análise, confirme o caminho do banco e abra-o em modo somente leitura.
### Abertura recomendada em Python
```python
import sqlite3
DB_PATH = "data/imoveis.sqlite"
connection = sqlite3.connect(
f"file:{DB_PATH}?mode=ro&immutable=1",
uri=True,
)
connection.execute("PRAGMA query_only = ON")
```
Se o banco estiver sendo atualizado por outro processo, não use `immutable=1`; mantenha `mode=ro` e confirme a consistência do snapshot antes de analisar.
### Cliente de linha de comando
```bash
sqlite3 -readonly data/imoveis.sqlite
```
---
## Fluxo inicial obrigatório
Ao iniciar uma nova tarefa de análise:
1. Identifique o arquivo SQLite correto e seu tamanho.
2. Leia `sqlite_master` para listar tabelas, views, índices e triggers.
3. Examine o esquema real antes de escrever consultas analíticas.
4. Conte registros por tabela.
5. Identifique campos de data, preço, área, quartos, localização, URL, anunciante e identificadores.
6. Verifique valores nulos, duplicidades, formatos inconsistentes e possíveis quebras de coleta.
7. Descubra se os dados representam:
- anúncios;
- imóveis físicos;
- observações históricas do mesmo anúncio;
- execuções do scraper.
8. Documente qualquer ambiguidade antes de produzir conclusões.
Não presuma que o modelo lógico descrito em `docs/DATA_MODEL.md` já existe fisicamente.
---
## Princípios de análise
- Diferencie sempre **anúncio**, **imóvel físico** e **observação histórica**.
- Um mesmo imóvel pode aparecer em vários anúncios, portais, corretores ou datas.
- Um mesmo anúncio pode mudar de preço, descrição, área informada ou status.
- Preço anunciado não é preço efetivamente negociado.
- Ausência de anúncio não prova venda.
- Remoção de anúncio não prova fechamento de negócio.
- Campo preenchido pelo anunciante não deve ser considerado confiável sem validação.
- Não calcule preço por metro quadrado quando a área não for comparável ou confiável.
- Não misture área privativa, útil, construída e total sem separar os conceitos.
- Não compare períodos sem verificar mudanças no universo coletado.
- Não compare regiões sem controlar tipologia, quartos, área, garagem, estado e padrão do imóvel.
- Sempre informe tamanho da amostra, cobertura temporal, filtros e limitações.
- Prefira mediana, percentis e intervalos interquartis a médias isoladas.
- Mostre a sensibilidade das conclusões a outliers, duplicidades e dados ausentes.
---
## Segurança e desempenho
- Nunca exponha credenciais, cookies, tokens, chaves, sessões ou dados pessoais em relatórios.
- Não envie o banco ou grandes amostras para serviços externos sem autorização explícita.
- Não tente contornar mecanismos de proteção de sites.
- Respeite limites de requisição, termos aplicáveis e regras definidas para o scraper.
- Evite consultas que façam varreduras desnecessárias repetidas em tabelas grandes.
- Use `EXPLAIN QUERY PLAN` em consultas lentas.
- Sugira índices apenas como recomendação; não os crie no banco original.
- Para transformações pesadas, crie um banco derivado ou arquivos Parquet/CSV separados.
---
## Código
- Escreva scripts reproduzíveis, parametrizados e idempotentes.
- Use caminhos relativos ao repositório.
- Evite constantes escondidas.
- Registre os filtros e premissas usados.
- Inclua tratamento de erros.
- Use consultas SQL parametrizadas.
- Adicione testes para regras de normalização, deduplicação e cálculo.
- Não altere arquivos sem relação com a tarefa.
- Não faça refatorações amplas sem necessidade.
- Não invente significado para colunas pouco claras; documente a dúvida.
---
## Saídas esperadas
Relatórios analíticos devem, sempre que aplicável, conter:
1. pergunta respondida;
2. período e universo analisados;
3. filtros e exclusões;
4. tamanho da amostra;
5. método de deduplicação;
6. qualidade dos dados;
7. resultados;
8. limitações;
9. implicações para a decisão de compra;
10. consultas ou scripts usados para reproduzir o resultado.
Use gráficos apenas quando eles melhorarem a interpretação. Todo gráfico deve ter título, unidade, período, tamanho da amostra e definição da métrica.
---
## Contexto imobiliário prioritário
As regiões prioritárias são:
1. Cruzeiro, especialmente Cruzeiro Novo;
2. Jardins Mangueiral.
Não exclua outras regiões de uma análise exploratória quando forem úteis como referência, mas não desvie o projeto de seu objetivo principal sem instrução.
Para Jardins Mangueiral:
- considerar somente casas com planta original de três quartos;
- não penalizar automaticamente o fechamento do quintal;
- avaliar a funcionalidade real da conversão;
- considerar as preferências de QC descritas em `docs/PROPERTY_CRITERIA.md`;
- tratar relatos de moradores como evidência anedótica, não como fato conclusivo.
---
## Atualização do contexto
Não altere estes documentos apenas porque encontrou um padrão no banco.
Atualize-os somente quando:
- o usuário corrigir uma premissa;
- uma preferência for explicitamente confirmada;
- um valor financeiro for atualizado;
- uma regra metodológica for aprovada;
- o esquema real do projeto for consolidado.
Ao atualizar uma premissa financeira, registre a data de referência e preserve o histórico em Git.

216
README.md

@ -0,0 +1,216 @@
# Busca de Imóveis DF
Projeto pessoal para coletar, organizar e analisar anúncios imobiliários do Distrito Federal, com foco em uma decisão residencial de longo prazo.
As regiões prioritárias são:
1. Cruzeiro, especialmente Cruzeiro Novo;
2. Jardins Mangueiral.
O objetivo não é identificar simplesmente o menor preço anunciado. A análise considera funcionalidade, estado do imóvel, risco de reforma, localização, liquidez, custos de aquisição e manutenção e adequação à vida familiar.
## Estado atual
O repositório contém:
- scraper autorizado para o portal DF Imóveis;
- banco SQLite com anúncios, snapshots de conteúdo, execuções de coleta e fotos;
- HTMLs brutos para reextração e auditoria;
- plantas e mapas de apoio do Jardins Mangueiral;
- relatórios reproduzíveis para priorização de visitas.
Relatórios principais:
- [Top 10 — Cruzeiro Novo](reports/cruzeiro_top10/README.md)
- [Top 10 — Jardins Mangueiral](reports/mangueiral_top10/README.md)
- [Cruzeiro Novo x Jardins Mangueiral](reports/cruzeiro_vs_mangueiral/README.md)
Os rankings são prioridades de investigação, não recomendações automáticas de compra nem estimativas de preço negociado.
## Princípios metodológicos
- Diferenciar anúncio, snapshot histórico e imóvel físico provável.
- Tratar preço como valor pedido, não como preço de transação.
- Não interpretar retirada de anúncio como prova de venda.
- Deduplicar republicações e anúncios do mesmo imóvel de forma reversível.
- Não calcular preço por metro quadrado quando o conceito de área não for comparável.
- Considerar fotos, descrições e HTML bruto, sem assumir que confirmam estrutura ou documentação.
- Excluir do ranking de visitas anúncios cuja primeira foto indique “vendido” ou “negócio fechado”, registrando-os em quarentena visual.
- Para o Jardins Mangueiral, considerar somente casas com planta original provável de três quartos.
- Não penalizar automaticamente quintais fechados; avaliar sua funcionalidade, ventilação e qualidade construtiva.
As regras completas estão em [docs/ANALYSIS_RULES.md](docs/ANALYSIS_RULES.md) e [docs/PROPERTY_CRITERIA.md](docs/PROPERTY_CRITERIA.md).
## Estrutura do repositório
```text
.
├── dfimoveis_scraper.py # coletor principal
├── search_list.txt # buscas executadas pelo scraper
├── requirements-dfimoveis.txt # dependências Python
├── dfimoveis_data/ # banco, fotos, HTML bruto e perfil do navegador
├── docs/ # contexto, critérios, modelo e regras
├── reports/ # relatórios e artefatos reproduzíveis
└── resources/
├── maps/ # capturas de mapas do Mangueiral
└── plans/ # plantas de referência de casas de dois e três quartos
```
Documentação essencial:
- [PROJECT_CONTEXT.md](docs/PROJECT_CONTEXT.md): objetivo, contexto residencial e premissas financeiras;
- [PROPERTY_CRITERIA.md](docs/PROPERTY_CRITERIA.md): critérios funcionais e de visita;
- [DATA_MODEL.md](docs/DATA_MODEL.md): modelo conceitual desejado;
- [ACTUAL_SCHEMA.md](docs/ACTUAL_SCHEMA.md): esquema físico observado;
- [ANALYSIS_RULES.md](docs/ANALYSIS_RULES.md): regras metodológicas;
- [AGENTS.md](AGENTS.md): instruções operacionais e de segurança.
## Requisitos
- Python 3.12 ou posterior;
- Google Chrome ou Microsoft Edge para a coleta assistida por navegador;
- PowerShell para os exemplos abaixo.
Crie um ambiente virtual e instale as dependências:
```powershell
python -m venv .venv-scraper
.\.venv-scraper\Scripts\python.exe -m pip install -r requirements-dfimoveis.txt
```
As ferramentas de similaridade fotográfica usam Pillow, incluído no arquivo de requisitos.
## Configuração das buscas
O arquivo [search_list.txt](search_list.txt) contém uma URL completa por linha. Atualmente ele cobre:
- apartamentos de três quartos no Cruzeiro Novo;
- casas anunciadas com três ou quatro quartos no Jardins Mangueiral.
A busca do Mangueiral inclui quatro quartos para ampliar a coleta, mas esses imóveis são excluídos da análise regional. Mesmo três quartos anunciados não comprovam a planta original.
## Executando o scraper
Use apenas em sites e condições para os quais você tenha autorização. Respeite termos aplicáveis, limites de requisição e mecanismos de proteção do portal.
### Navegador iniciado pelo scraper
```powershell
.\.venv-scraper\Scripts\python.exe .\dfimoveis_scraper.py `
--search-file .\search_list.txt `
--output .\dfimoveis_data
```
### Chrome iniciado manualmente
Quando necessário, inicie uma instância separada do Chrome com depuração remota. Não use o perfil cotidiano do navegador.
```powershell
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9222 `
--user-data-dir="E:\forge\python\house-quest\dfimoveis_data\browser-profile"
```
Em outro terminal:
```powershell
.\.venv-scraper\Scripts\python.exe .\dfimoveis_scraper.py `
--search-file .\search_list.txt `
--output .\dfimoveis_data `
--attach-chrome
```
Consulte todas as opções com:
```powershell
.\.venv-scraper\Scripts\python.exe .\dfimoveis_scraper.py --help
```
Não execute o scraper enquanto uma análise espera um snapshot estável do banco.
## Banco de dados
O banco principal fica em:
```text
dfimoveis_data/dfimoveis.sqlite3
```
Ele deve ser tratado como estritamente somente leitura durante análises. Nunca grave resultados derivados dentro dele.
Quando não houver processo de coleta e não existir WAL relevante:
```python
import sqlite3
connection = sqlite3.connect(
"file:dfimoveis_data/dfimoveis.sqlite3?mode=ro&immutable=1",
uri=True,
)
connection.execute("PRAGMA query_only = ON")
```
Quando existir `dfimoveis.sqlite3-wal` ou houver possibilidade de atualização concorrente, não use `immutable=1`:
```python
connection = sqlite3.connect(
"file:dfimoveis_data/dfimoveis.sqlite3?mode=ro",
uri=True,
)
connection.execute("PRAGMA query_only = ON")
connection.execute("BEGIN")
```
Não remova arquivos `-wal` ou `-shm`. Consulte [ACTUAL_SCHEMA.md](docs/ACTUAL_SCHEMA.md) antes de escrever consultas analíticas.
## Reproduzindo os relatórios
Cada relatório regional contém:
```text
reports/<relatorio>/
├── README.md
├── analysis.py
├── photo_similarity.py
├── query.sql
├── parameters.json
└── results.csv
```
Extração tabular somente leitura:
```powershell
.\.venv-scraper\Scripts\python.exe .\reports\cruzeiro_top10\analysis.py
.\.venv-scraper\Scripts\python.exe .\reports\mangueiral_top10\analysis.py
```
Triagem de possíveis duplicatas por similaridade visual:
```powershell
.\.venv-scraper\Scripts\python.exe .\reports\cruzeiro_top10\photo_similarity.py
.\.venv-scraper\Scripts\python.exe .\reports\mangueiral_top10\photo_similarity.py
```
Similaridade de imagem é apenas um sinal para revisão humana, nunca prova automática de que dois anúncios representam o mesmo imóvel.
## Limitações
- A cobertura atual usa um único portal e poucas datas de coleta.
- O banco representa anúncios e mudanças de conteúdo, não transações imobiliárias.
- Localização e campos de anunciante apresentam contaminação de extração documentada.
- A área estruturada não informa de forma segura se é útil, privativa, construída, total ou de terreno.
- Fotos podem conter material genérico, itens de outros anúncios e selos de venda.
- Fotos não confirmam documentação, sistemas elétricos e hidráulicos, impermeabilização, ruído ou conforto térmico.
- A situação financeira registrada deve ser atualizada antes de proposta, financiamento ou definição de preço máximo.
## Segurança e privacidade
- Não publique credenciais, cookies, tokens, sessões ou perfis do navegador.
- Não envie o banco ou grandes amostras a serviços externos sem autorização.
- Não tente contornar mecanismos de proteção de sites.
- O diretório `dfimoveis_data/` é ignorado pelo Git e deve permanecer local.
- Revise qualquer artefato derivado antes de versioná-lo para evitar dados pessoais.
## Finalidade
Este projeto apoia uma decisão pessoal de compra residencial. Ele não constitui avaliação imobiliária profissional, parecer jurídico, inspeção de engenharia ou recomendação financeira.

BIN
__pycache__/dfimoveis_scraper.cpython-312.pyc

Binary file not shown.

BIN
__pycache__/dfimoveis_scraper.cpython-314.pyc

Binary file not shown.

1215
dfimoveis_scraper.py

File diff suppressed because it is too large Load Diff

238
docs/ACTUAL_SCHEMA.md

@ -0,0 +1,238 @@
# Esquema físico do banco `dfimoveis.sqlite3`
## 1. Escopo e identificação do snapshot
Este documento descreve o esquema observado em 13 de julho de 2026. Ele não altera o modelo conceitual de `docs/DATA_MODEL.md` e não atribui significado não comprovado às colunas.
| Item | Valor |
|---|---|
| Caminho | `dfimoveis_data/dfimoveis.sqlite3` |
| Tamanho | 5.017.600 bytes |
| SHA-256 | `1ce3128b488c50c993d7328ac6e24b5905f083a7e04e3f636407eea1edd02b9d` |
| Modificação do arquivo | `2026-07-13 14:20:34.601285975 -03:00` |
| SQLite | 3.45.1 |
| Codificação | UTF-8 |
| `journal_mode` persistido | WAL |
| Páginas | 1.225 páginas de 4.096 bytes; `freelist_count = 0` |
| `user_version` / `application_id` | 0 / 0 |
| Método de abertura | `sqlite3 -readonly` e URI `mode=ro&immutable=1`, sempre com `PRAGMA query_only = ON` |
Não havia arquivo `-wal` ou `-shm` nem processo com o banco aberto na verificação inicial. Isso permitiu tratar o conteúdo como snapshot imutável. `PRAGMA quick_check` retornou `ok` e `PRAGMA foreign_key_check` não retornou violações. Na validação final, o cliente SQLite havia criado os sidecars de runtime `dfimoveis.sqlite3-shm` (32.768 bytes) e `dfimoveis.sqlite3-wal` (vazio), comportamento possível ao abrir em leitura um banco configurado em WAL. Eles não foram removidos. O arquivo principal manteve tamanho, `mtime` e SHA-256 idênticos.
## 2. Visão geral
O banco possui cinco tabelas da aplicação e a tabela interna `sqlite_sequence`. Não há views nem triggers. Os quatro índices existentes são autoíndices criados por chaves primárias ou restrições `UNIQUE`.
```mermaid
erDiagram
listings ||--|{ listing_history : "listing_id"
listings ||--|{ listing_searches : "listing_id"
listings ||--|{ photos : "listing_id"
crawl_runs {
INTEGER id PK
TEXT started_at
TEXT finished_at
TEXT status
TEXT stats_json
}
listings {
TEXT listing_id PK
TEXT url
REAL price_brl
REAL area_m2
TEXT first_seen_at
TEXT last_seen_at
TEXT content_hash
}
listing_history {
INTEGER id PK
TEXT listing_id FK
TEXT captured_at
TEXT content_hash
TEXT data_json
}
listing_searches {
TEXT listing_id PK,FK
TEXT search_url PK
TEXT first_seen_at
TEXT last_seen_at
}
photos {
TEXT listing_id PK,FK
TEXT source_url PK
INTEGER ordinal
TEXT sha256
TEXT local_path
}
```
`crawl_runs` não possui relacionamento físico com anúncios ou snapshots. Portanto, a execução que gerou uma linha só pode ser inferida pelos horários, não recuperada por uma chave.
### Contagens e cardinalidades observadas
| Tabela | Linhas | Interpretação física |
|---|---:|---|
| `crawl_runs` | 4 | Execuções do coletor |
| `listings` | 156 | Identidades de anúncios e seu estado corrente |
| `listing_history` | 156 | Estados distintos dos anúncios por hash |
| `listing_searches` | 156 | Vínculos entre anúncio e URL de busca |
| `photos` | 5.386 | Mídias associadas diretamente aos anúncios |
| `sqlite_sequence` | 2 | Sequências internas de tabelas `AUTOINCREMENT` |
No snapshot avaliado:
- cada anúncio possui exatamente um registro em `listing_history`;
- cada anúncio está ligado a exatamente uma URL de busca;
- cada anúncio possui de 5 a 61 linhas em `photos`;
- não há órfãos nas três relações com `listings`;
- os 156 hashes de `listing_history` coincidem com o hash corrente em `listings`;
- `captured_at` coincide com `first_seen_at` nos 156 anúncios.
## 3. Dicionário de tabelas
### 3.1 `crawl_runs`
Representa execuções técnicas do coletor. Não registra fonte, versão do scraper, configuração, texto de erro ou relacionamento direto com os itens coletados.
| Coluna | Tipo | Nulo? | Chave/default | Interpretação |
|---|---|---:|---|---|
| `id` | INTEGER | Não | PK, `AUTOINCREMENT` | Identificador da execução; a PK inteira atribui um valor quando omitido |
| `started_at` | TEXT | Não | — | Início em ISO 8601, observado em UTC |
| `finished_at` | TEXT | Sim | — | Término em ISO 8601 |
| `status` | TEXT | Não | — | Estado textual da execução; somente `ok` na amostra |
| `stats_json` | TEXT | Não | `'{}'` | Contadores técnicos em JSON |
As quatro estruturas JSON são válidas. As chaves presentes são `search_pages`, `listing_urls`, `details_ok`, `details_failed`, `changed`, `photos_ok`, `photos_failed` e `inactive`, todas inteiras.
### 3.2 `listings`
Representa a identidade do anúncio no portal e uma cópia mutável de seu estado mais recente. Não representa um imóvel físico deduplicado.
| Coluna | Tipo | Nulo? | Chave/default | Interpretação e ressalvas |
|---|---|---:|---|---|
| `listing_id` | TEXT | Sim no DDL; não observado | PK | Identificador do anúncio no portal; todos os valores atuais têm 6–7 dígitos. Por peculiaridade do SQLite, PK textual sem `NOT NULL` explícito merece validação na aplicação |
| `url` | TEXT | Não | — | URL do anúncio |
| `title` | TEXT | Sim | — | Título extraído |
| `description` | TEXT | Sim | — | Descrição curta extraída; não é o HTML bruto completo |
| `transaction_type` | TEXT | Sim | — | Tipo da transação; somente `venda` no snapshot |
| `property_type` | TEXT | Sim | — | Tipo anunciado; `apartamento` ou `casa` |
| `price_brl` | REAL | Sim | — | Preço anunciado em reais; sujeito a erros de escala |
| `condominium_brl` | REAL | Sim | — | Condomínio anunciado; periodicidade não está formalizada |
| `iptu_brl` | REAL | Sim | — | IPTU anunciado; periodicidade não está formalizada |
| `area_m2` | REAL | Sim | — | Uma única área em m², sem indicar se é útil, privativa, construída, total ou terreno |
| `bedrooms` | INTEGER | Sim | — | Quartos anunciados, não a planta original comprovada |
| `suites` | INTEGER | Sim | — | Suítes anunciadas |
| `parking_spaces` | INTEGER | Sim | — | Vagas anunciadas |
| `address` | TEXT | Sim | — | Texto de endereço; fortemente contaminado pela página na coleta atual |
| `neighborhood` | TEXT | Sim | — | Bairro; fortemente contaminado pela página na coleta atual |
| `city` | TEXT | Sim | — | Cidade; 100% nula atualmente |
| `state` | TEXT | Sim | — | UF; 100% nula atualmente |
| `advertiser_name` | TEXT | Sim | — | Nome do anunciante; 100% nulo atualmente |
| `advertiser_code` | TEXT | Sim | — | Código do anunciante; parte relevante dos valores está contaminada |
| `creci` | TEXT | Sim | — | Registro do anunciante; parte relevante dos valores está contaminada |
| `latitude` | REAL | Sim | — | Latitude; 100% nula atualmente |
| `longitude` | REAL | Sim | — | Longitude; 100% nula atualmente |
| `published_at` | TEXT | Sim | — | Data publicada pelo portal; 100% nula atualmente |
| `first_seen_at` | TEXT | Não | — | Primeira observação do anúncio |
| `last_seen_at` | TEXT | Não | — | Última observação do anúncio |
| `inactive_at` | TEXT | Sim | — | Momento de inativação inferida pelo coletor; 100% nulo atualmente |
| `content_hash` | TEXT | Sim | — | SHA-256 do conteúdo normalizado usado para detectar mudança |
| `raw_html_path` | TEXT | Sim | — | Caminho relativo para o HTML bruto do anúncio |
| `extra_json` | TEXT | Não | `'{}'` | JSON adicional; contém apenas `json_ld`, um array vazio nos 156 anúncios |
Exemplo não sensível de identidade: o anúncio `1026522` aponta para uma URL sob `www.dfimoveis.com.br` e para um HTML em `raw/listing/1026522/...html.gz`.
### 3.3 `listing_history`
Representa estados distintos de um anúncio, não cada tentativa ou observação do coletor.
| Coluna | Tipo | Nulo? | Chave/default | Interpretação |
|---|---|---:|---|---|
| `id` | INTEGER | Não | PK, `AUTOINCREMENT` | Identificador do estado histórico; a PK inteira atribui um valor quando omitido |
| `listing_id` | TEXT | Não | FK | Anúncio ao qual o estado pertence |
| `captured_at` | TEXT | Não | — | Momento em que o estado foi capturado |
| `content_hash` | TEXT | Não | `UNIQUE` com `listing_id` | Hash que impede repetir um estado idêntico para o anúncio |
| `data_json` | TEXT | Não | — | Snapshot JSON dos principais campos extraídos |
`data_json` é JSON válido em todas as 156 linhas. Ele contém os campos correntes de conteúdo, mas não contém `first_seen_at`, `last_seen_at`, `inactive_at`, `raw_html_path` ou uma referência à execução. Como a restrição é `UNIQUE(listing_id, content_hash)`, observações repetidas sem mudança não geram novos snapshots.
### 3.4 `listing_searches`
Tabela de associação entre anúncio e consulta de busca que o encontrou.
| Coluna | Tipo | Nulo? | Chave/default | Interpretação |
|---|---|---:|---|---|
| `listing_id` | TEXT | Não | PK parcial, FK | Anúncio |
| `search_url` | TEXT | Não | PK parcial | URL/configuração da busca |
| `first_seen_at` | TEXT | Não | — | Primeira observação nessa busca |
| `last_seen_at` | TEXT | Não | — | Última observação nessa busca |
A chave primária composta é `(listing_id, search_url)`. Há duas URLs distintas no banco, uma para apartamentos de três quartos no Cruzeiro Novo e outra para casas anunciadas com três ou quatro quartos no Jardins Mangueiral.
### 3.5 `photos`
Representa arquivos de mídia associados diretamente ao anúncio. A tabela não preserva associação por snapshot.
| Coluna | Tipo | Nulo? | Chave/default | Interpretação |
|---|---|---:|---|---|
| `listing_id` | TEXT | Não | PK parcial, FK | Anúncio |
| `ordinal` | INTEGER | Não | — | Ordem da imagem no anúncio |
| `source_url` | TEXT | Não | PK parcial | URL de origem da imagem |
| `sha256` | TEXT | Sim | — | Hash do arquivo baixado |
| `mime_type` | TEXT | Sim | — | Tipo MIME detectado |
| `bytes` | INTEGER | Sim | — | Tamanho do arquivo |
| `local_path` | TEXT | Sim | — | Caminho relativo sob `dfimoveis_data/` |
| `first_seen_at` | TEXT | Não | — | Primeira observação da mídia |
| `last_seen_at` | TEXT | Não | — | Última observação da mídia |
A chave primária é `(listing_id, source_url)`, e não `(listing_id, ordinal)`. Não há ordinais repetidos dentro de um anúncio na amostra atual. Exemplo de caminho: `photos/1026522/001_9d7f11205f0a4a3f.jpg`.
### 3.6 `sqlite_sequence`
Tabela interna do SQLite que mantém as sequências `AUTOINCREMENT` de `crawl_runs` e `listing_history`. Não é entidade do domínio.
## 4. Chaves, índices e integridade referencial
| Índice | Tabela | Origem | Colunas lógicas |
|---|---|---|---|
| `sqlite_autoindex_listings_1` | `listings` | PK | `listing_id` |
| `sqlite_autoindex_listing_history_1` | `listing_history` | `UNIQUE` | `listing_id`, `content_hash` |
| `sqlite_autoindex_listing_searches_1` | `listing_searches` | PK | `listing_id`, `search_url` |
| `sqlite_autoindex_photos_1` | `photos` | PK | `listing_id`, `source_url` |
As FKs de `listing_history`, `listing_searches` e `photos` apontam para `listings(listing_id)`, com `NO ACTION` para atualização e exclusão. Não há índices explícitos por data, preço, região ou hash de foto. Com o volume atual isso não é um problema operacional; se a base crescer, índices analíticos devem ser criados apenas em uma camada derivada ou recomendados para uma migração aprovada, nunca adicionados ao banco original durante análise.
## 5. Correspondência com o modelo conceitual
| Conceito desejado | Implementação física | Avaliação |
|---|---|---|
| Fonte | Ausente; DF Imóveis é implícito nas URLs | Não permite múltiplas fontes com identidade segura |
| Execução de coleta | `crawl_runs` | Parcial; faltam fonte, configuração, versão e relação com itens |
| Anúncio | `listings` | Presente; estado corrente e identidade estão na mesma linha |
| Observação histórica | `listing_history` | Parcial; guarda somente conteúdo alterado, sem `run_id` |
| Imóvel físico candidato | Ausente | Nenhuma deduplicação física é persistida |
| Localização normalizada | Colunas em `listings` | Insuficiente e atualmente contaminada |
| Mídia | `photos` | Parcial; vinculada ao anúncio, não ao snapshot |
| Origem por consulta | `listing_searches` | Presente como relação anúncio–URL de busca |
## 6. Decisões de interpretação e ambiguidades
1. `listing_id` identifica um **anúncio no portal**, não um imóvel físico.
2. Uma linha de `listing_history` é um **estado de conteúdo distinto**, não prova de que o anúncio foi observado somente uma vez. Observações idênticas são condensadas em `last_seen_at`.
3. O banco ainda não contém histórico suficiente para preços ou permanência: há um snapshot por anúncio e todos os anúncios têm `first_seen_at = last_seen_at`.
4. Anúncios diferentes com preço, área ou fotos semelhantes permanecem anúncios distintos. Não há chave de endereço/unidade nem tabela de candidatos físicos.
5. `area_m2` não deve ser usada para preço por m² até que o conceito de área seja validado por tipologia e fonte.
6. `bedrooms` é a quantidade anunciada. Ela não comprova a planta original de três quartos exigida para Jardins Mangueiral.
7. Datas com sufixo `+00:00` são tratadas como UTC. A cobertura observada é de um único dia.
8. Os campos textuais contaminados devem ser reextraídos do HTML bruto em uma camada derivada; os valores atuais não devem ser silenciosamente normalizados como se fossem corretos.
## 7. Problemas conhecidos do snapshot
- 146 de 156 endereços têm mais de 300 caracteres; 154 de 156 bairros têm mais de 100 caracteres. Os dois bairros curtos também não formam valores confiáveis.
- `city`, `state`, `latitude`, `longitude`, `published_at` e `advertiser_name` estão integralmente ausentes.
- 98 códigos de anunciante e 153 valores de CRECI são longos demais para o significado esperado, indicando extração contaminada.
- Há um preço de R$ 570.000.000 e uma área de 0,11 m², ambos candidatos fortes a erro de extração/escala.
- O JSON-LD reservado em `extra_json` é um array vazio em todos os anúncios e não oferece uma fonte estruturada alternativa no snapshot.
- Três downloads de foto falharam e deixaram linhas sem hash, MIME, tamanho e caminho local.
- Hashes de foto idênticos aparecem em muitos anúncios, inclusive um hash presente nos 156; isso demonstra a presença de ativos genéricos e impede usar igualdade de hash como prova isolada de imóvel duplicado.
- Não há como ligar formalmente anúncio/snapshot a `crawl_runs`, nem distinguir por chave a fonte se uma segunda fonte for incorporada.

542
docs/ANALYSIS_RULES.md

@ -0,0 +1,542 @@
# Regras de análise
## 1. Objetivo metodológico
Produzir análises reproduzíveis e úteis para decisão de compra, sem transformar dados de anúncios em uma falsa precisão de mercado.
Toda conclusão deve distinguir:
- o que os dados mostram;
- o que foi inferido;
- o que permanece desconhecido;
- o que depende de visita, documentação ou negociação.
---
## 2. Unidade de análise
Definir antes de calcular:
- anúncio único;
- snapshot de anúncio;
- imóvel físico provável;
- imóvel físico confirmado;
- região;
- tipologia;
- período.
Não misturar unidades.
Exemplo de erro:
> Contar cada snapshot mensal como um imóvel diferente.
Exemplo correto:
> Para estoque histórico, usar um anúncio ativo por data; para oferta única, deduplicar anúncios e imóveis prováveis.
---
## 3. Cobertura e consistência temporal
Antes de comparar meses:
- verificar dias coletados;
- verificar fontes ativas;
- verificar falhas;
- verificar mudanças do scraper;
- verificar novos filtros;
- verificar alterações no site;
- verificar interrupções;
- verificar mudanças de campos.
Uma queda no número de anúncios pode ser falha de coleta.
Relatórios temporais devem exibir cobertura.
---
## 4. Limpeza
### 4.1 Preços inválidos
Identificar:
- zero;
- preço mensal;
- preço de aluguel em anúncio de venda;
- valor parcial;
- erro de escala;
- preço simbólico;
- preço por fração;
- moeda incorreta;
- preço inconsistente com descrição.
Não remover silenciosamente. Registrar regra e contagem.
### 4.2 Áreas inválidas
Identificar:
- zero;
- área total usada como privativa;
- área do terreno;
- área multiplicada;
- vírgula decimal interpretada incorretamente;
- área do condomínio;
- área incompatível com a tipologia.
### 4.3 Quartos e tipologia
Verificar divergências entre:
- título;
- campos estruturados;
- descrição;
- planta;
- fotografias;
- tipologia original.
Para Jardins Mangueiral, não aceitar tipologia anunciada como prova da planta original.
---
## 5. Preço por metro quadrado
Calcular somente quando:
- o preço é válido;
- a área é válida;
- a área representa conceito comparável;
- o imóvel pertence a uma tipologia comparável.
Registrar a área usada.
Não comparar diretamente:
- área privativa com área total;
- casa com apartamento;
- imóvel reformado com imóvel para reforma sem controle;
- dois quartos com três quartos apenas pela média regional;
- cobertura com unidade padrão;
- casa ampliada com planta original sem ajustar a área.
Estatísticas mínimas:
- número de observações;
- mediana;
- percentil 25;
- percentil 75;
- mínimo e máximo após validação;
- proporção excluída.
---
## 6. Outliers
Não excluir outliers apenas porque são extremos.
Investigar se representam:
- erro;
- imóvel premium;
- reforma;
- venda urgente;
- leilão;
- área incorreta;
- anúncio duplicado;
- preço desatualizado;
- unidade atípica.
Apresentar resultados com e sem outliers quando a conclusão mudar.
Métodos permitidos:
- IQR;
- MAD;
- limites específicos por tipologia;
- inspeção manual;
- modelos robustos.
Não usar corte automático sem explicar.
---
## 7. Duplicidades
Relatar:
- duplicatas dentro da mesma fonte;
- duplicatas entre fontes;
- snapshots repetidos;
- imóveis anunciados por vários corretores;
- anúncios republicados com novo identificador.
Para preço de oferta atual, escolher uma regra explícita, por exemplo:
- menor preço vigente por imóvel provável;
- anúncio mais recente;
- anúncio primário;
- mediana entre anúncios do mesmo imóvel.
Testar sensibilidade da conclusão à regra.
---
## 8. Histórico de anúncios
Para cada anúncio ou imóvel provável, tentar identificar:
- primeira aparição;
- última aparição;
- tempo observado;
- mudanças de preço;
- períodos de ausência;
- republicação;
- alteração de corretor;
- alteração de descrição;
- alteração de área ou quartos.
Não concluir “vendido” apenas porque desapareceu.
Estados possíveis:
- ativo;
- ausente na coleta;
- removido;
- republicado;
- provável venda;
- status desconhecido.
“Provável venda” exige sinais adicionais.
---
## 9. Comparáveis
Um conjunto de comparáveis deve controlar, quando possível:
- região;
- quadra, bloco ou QC;
- tipo de imóvel;
- planta original;
- quartos;
- área;
- garagem;
- elevador;
- andar;
- posição;
- reforma;
- conservação;
- data;
- condomínio;
- situação de ocupação.
Quando não for possível controlar, declarar a limitação.
Nunca usar uma média ampla do bairro como avaliação definitiva de uma unidade específica.
---
## 10. Tendências
Para avaliar tendência de preço:
- usar imóveis comparáveis;
- controlar mudanças no mix;
- ponderar cobertura;
- evitar média simples;
- usar mediana e regressões robustas quando necessário;
- separar preço de entrada, preço atual e último preço;
- mostrar intervalo de confiança ou dispersão;
- evitar extrapolação longa.
Uma alta no preço mediano pode ocorrer porque imóveis menores desapareceram da amostra.
---
## 11. Estoque e liquidez
Métricas úteis:
- anúncios ativos;
- imóveis prováveis ativos;
- entradas;
- saídas;
- republicações;
- tempo observado;
- reduções de preço;
- percentual com múltiplas reduções;
- concentração por corretor;
- dispersão de preços;
- taxa de reposição do estoque.
Limitações:
- tempo observado não é tempo real no mercado quando a coleta começou depois;
- retirada não confirma venda;
- republicação pode reiniciar o relógio artificialmente.
---
## 12. Análise financeira
### 12.1 Premissas obrigatórias
Toda simulação deve declarar:
- preço;
- entrada em dinheiro;
- FGTS;
- reserva preservada;
- custos de aquisição;
- reforma;
- valor financiado;
- taxa efetiva;
- CET, se disponível;
- sistema;
- prazo;
- seguros e tarifas;
- condomínio;
- IPTU;
- manutenção;
- reajuste do aluguel;
- retorno líquido dos investimentos;
- inflação;
- valorização;
- horizonte;
- custos de venda;
- estratégia de amortização.
### 12.2 Liquidez
Separar:
- dinheiro;
- investimentos com liquidez;
- FGTS;
- patrimônio imobiliário;
- reserva mínima.
Não tratar FGTS como reserva de emergência.
### 12.3 Compra versus aluguel
Comparar:
- fluxo mensal;
- patrimônio líquido;
- custo de oportunidade;
- risco;
- liquidez;
- flexibilidade;
- estabilidade;
- manutenção;
- impostos;
- custos de transação.
Mostrar cenários:
- conservador;
- central;
- otimista.
Não decidir apenas pelo patrimônio final médio.
### 12.4 Estresse
Testar:
- juros maiores;
- renda menor;
- despesas extraordinárias;
- reforma acima do orçamento;
- vacância ou dificuldade de revenda;
- valorização baixa;
- investimento com retorno menor;
- manutenção elevada;
- redução da capacidade de poupança.
---
## 13. Consórcio
Para analisar um grupo:
- usar histórico do próprio grupo;
- registrar assembleias;
- lances vencedores;
- lance livre;
- lance fixo;
- lance embutido;
- número de contemplações;
- saldo;
- reajuste da carta;
- taxa de administração;
- fundo de reserva;
- seguros;
- regras de uso;
- prazo;
- valor total pago.
Não inferir probabilidade de contemplação usando apenas média histórica.
Analisar:
- distribuição;
- percentis;
- sazonalidade;
- número de concorrentes;
- mudanças de regra;
- eventos de renda;
- concentração de lances;
- sensibilidade ao valor ofertado.
Relatos de gerente são hipóteses a testar.
---
## 14. Relatórios por região
### Cruzeiro
Separar por:
- Cruzeiro Novo e Velho;
- quadra;
- bloco;
- tipologia;
- área;
- elevador;
- garagem;
- andar;
- conservação;
- reforma.
### Jardins Mangueiral
Separar por:
- QC;
- planta original de três quartos;
- área construída;
- ampliação;
- qualidade da área fechada;
- condomínio;
- posição;
- ruído;
- proximidade de serviços;
- padrão de reforma.
Excluir casas originalmente de dois quartos.
---
## 15. Relatório padrão
### Resumo executivo
- conclusão principal;
- confiança;
- implicação prática;
- maior risco.
### Dados
- fontes;
- período;
- cobertura;
- tamanho da amostra;
- exclusões;
- deduplicação.
### Resultados
- tabelas;
- distribuições;
- comparáveis;
- tendências;
- sensibilidade.
### Interpretação
- o que é sustentado;
- o que é apenas sugestivo;
- o que requer visita;
- o que requer confirmação financeira ou documental.
### Próxima ação
Classificar como:
- ignorar;
- monitorar;
- aprofundar;
- visitar;
- negociar;
- candidato forte;
- decisão insuficiente.
---
## 16. Reprodutibilidade
Para cada relatório, salvar:
```text
reports/<nome>/
├── README.md
├── query.sql
├── analysis.py
├── parameters.json
├── results.csv
└── figures/
```
Registrar:
- data;
- hash do código;
- identificação do banco;
- filtros;
- versão;
- ambiente;
- contagens.
---
## 17. Linguagem das conclusões
Preferir:
- “os anúncios observados sugerem”;
- “na amostra coletada”;
- “após deduplicação”;
- “não há evidência suficiente”;
- “a conclusão é sensível a”;
- “o dado não permite afirmar”.
Evitar:
- “o mercado vale”;
- “foi vendido” sem prova;
- “esta é a melhor quadra” sem critério;
- “o preço justo é” sem intervalo e comparáveis;
- “vai valorizar” como certeza;
- “o lance será contemplado” como previsão.
---
## 18. Critério final
Uma análise é boa quando ajuda a evitar:
- comprar cedo demais;
- pagar por uma reforma disfarçada;
- confundir anúncio com transação;
- superestimar valorização;
- subestimar custos;
- comprometer liquidez;
- aceitar uma planta inadequada;
- tomar decisão por ansiedade;
- descartar um bom imóvel por uma métrica superficial.

5
docs/CHAT_HANDOFF.md

@ -0,0 +1,5 @@
# Chat handoff
## Operational transcript from the previous session
This file consolidates the prior working session into a handoff format so the project can continue without redoing the same analysis from scratch.

481
docs/DATA_MODEL.md

@ -0,0 +1,481 @@
# Modelo de dados — referência conceitual
## 1. Aviso
Este documento descreve o modelo lógico desejável para análise.
Ele **não afirma** que o banco SQLite atual possui essas tabelas ou colunas. O esquema real deve ser inspecionado antes de qualquer consulta.
Não migre o banco original para este modelo sem autorização.
---
## 2. Entidades fundamentais
O projeto deve distinguir cinco conceitos:
1. **fonte**: site, portal, corretor ou canal;
2. **anúncio**: publicação identificável em uma fonte;
3. **observação**: estado do anúncio em uma data de coleta;
4. **imóvel físico**: unidade residencial que pode aparecer em vários anúncios;
5. **execução de coleta**: processo técnico que obteve os dados.
Essa separação é essencial para séries históricas e deduplicação.
---
## 3. Fonte
Entidade conceitual: `source`
Campos desejáveis:
- `source_id`
- `name`
- `base_url`
- `source_type`
- `active`
- `notes`
- `created_at`
Não usar domínio ou nome de corretor como identificador único sem normalização.
---
## 4. Execução de coleta
Entidade conceitual: `scrape_run`
Campos desejáveis:
- `run_id`
- `source_id`
- `started_at`
- `finished_at`
- `status`
- `pages_requested`
- `pages_succeeded`
- `pages_failed`
- `items_seen`
- `items_inserted`
- `items_updated`
- `http_status_summary`
- `scraper_version`
- `configuration_hash`
- `error_summary`
Serve para identificar:
- períodos sem coleta;
- mudanças de cobertura;
- falhas;
- alterações de código;
- diferenças entre ausência real e ausência causada por erro.
---
## 5. Anúncio
Entidade conceitual: `listing`
Representa a identidade do anúncio, não cada coleta.
Campos desejáveis:
- `listing_id`
- `source_id`
- `source_listing_id`
- `canonical_url`
- `first_seen_at`
- `last_seen_at`
- `last_successful_seen_at`
- `status`
- `seller_type`
- `seller_name_normalized`
- `broker_registration`
- `property_candidate_id`
- `created_at`
- `updated_at`
Chave natural potencial:
```text
(source_id, source_listing_id)
```
A URL não deve ser a única chave, pois pode mudar.
---
## 6. Observação histórica
Entidade conceitual: `listing_snapshot`
Representa o conteúdo observado em uma execução.
Campos desejáveis:
- `snapshot_id`
- `listing_id`
- `run_id`
- `observed_at`
- `title`
- `description`
- `price`
- `condominium_fee`
- `property_tax`
- `area_private`
- `area_useful`
- `area_built`
- `area_total`
- `bedrooms`
- `suites`
- `bathrooms`
- `parking_spaces`
- `floor`
- `elevator`
- `furnished`
- `property_type`
- `address_raw`
- `neighborhood_raw`
- `latitude`
- `longitude`
- `image_count`
- `status_raw`
- `payload_hash`
- `raw_payload_reference`
Não sobrescrever o histórico quando um anúncio muda.
Se os dados atuais usam uma única linha mutável por anúncio, documentar essa limitação.
---
## 7. Imóvel físico candidato
Entidade conceitual: `property_candidate`
Representa uma hipótese de que anúncios diferentes descrevem o mesmo imóvel.
Campos desejáveis:
- `property_candidate_id`
- `canonical_region`
- `canonical_address`
- `canonical_unit`
- `property_type`
- `original_floor_plan`
- `dedup_confidence`
- `dedup_method`
- `review_status`
- `created_at`
- `updated_at`
Não tratar correspondência probabilística como certeza.
Estados sugeridos:
- `unreviewed`
- `probable`
- `confirmed`
- `rejected`
- `ambiguous`
---
## 8. Endereço e localização
Entidade conceitual: `location`
Campos desejáveis:
- `location_id`
- `region`
- `administrative_region`
- `neighborhood`
- `superquadra`
- `quadra`
- `block`
- `qc`
- `street`
- `lot`
- `unit`
- `postal_code`
- `latitude`
- `longitude`
- `geocode_precision`
- `geocode_source`
No Distrito Federal, o endereço deve permitir estruturas como:
- SHCES;
- quadra;
- bloco;
- superquadra;
- conjunto;
- lote;
- QC;
- condomínio;
- unidade.
Não forçar todos os endereços a um modelo de rua e número.
---
## 9. Mídia
Entidade conceitual: `listing_media`
Campos desejáveis:
- `media_id`
- `snapshot_id`
- `media_url`
- `media_type`
- `position`
- `content_hash`
- `perceptual_hash`
- `width`
- `height`
- `download_status`
Hashes perceptuais de imagens podem ajudar na deduplicação, mas não devem decidir sozinhos.
---
## 10. Histórico de preço
O histórico pode ser derivado de `listing_snapshot`.
View conceitual:
```sql
SELECT
listing_id,
observed_at,
price,
price - LAG(price) OVER (
PARTITION BY listing_id
ORDER BY observed_at
) AS absolute_change
FROM listing_snapshot;
```
Regras:
- ignorar repetições idênticas;
- registrar aumentos e reduções;
- não interpretar ausência como venda;
- identificar alterações simultâneas de área ou tipologia;
- detectar preços promocionais artificiais;
- separar moeda e unidades.
---
## 11. Deduplicação
### 11.1 Sinais fortes
- mesmo identificador na fonte;
- mesmo endereço e unidade;
- mesmo conjunto de imagens;
- mesmo telefone ou corretor com descrição muito semelhante;
- mesma planta, metragem e características;
- coordenadas muito próximas;
- texto quase idêntico.
### 11.2 Sinais moderados
- mesma quadra e bloco;
- mesmo preço;
- mesma área;
- mesmos quartos e vagas;
- datas próximas;
- imagens parcialmente coincidentes.
### 11.3 Sinais fracos
- mesmo bairro;
- título genérico;
- preço arredondado;
- número de quartos isolado.
### 11.4 Regras
- manter o anúncio original;
- criar relacionamento de duplicidade;
- registrar método e confiança;
- permitir revisão manual;
- não fundir registros de forma irreversível;
- não usar apenas texto normalizado;
- evitar que atualizações do mesmo anúncio sejam contadas como imóveis novos.
---
## 12. Normalização
### Preço
- armazenar como número inteiro em centavos ou valor monetário consistente;
- preservar valor bruto;
- registrar moeda;
- detectar preço por mês confundido com preço total;
- detectar valores incompletos.
### Área
Manter campos separados:
- privativa;
- útil;
- construída;
- total;
- terreno.
Nunca preencher uma área com outra apenas para completar dados.
### Quartos
Separar:
- quartos;
- suítes;
- dependência;
- escritório;
- quarto reversível;
- tipologia original;
- tipologia anunciada.
### Localização
Preservar:
- texto bruto;
- versão normalizada;
- componentes extraídos;
- confiança da extração.
### Datas
Usar ISO 8601 e timezone explícito quando relevante.
---
## 13. Qualidade dos dados
Criar métricas por fonte, período e região:
- percentual sem preço;
- percentual sem área;
- percentual sem localização;
- percentual com preço por m² calculável;
- duplicidade estimada;
- taxa de mudança de identificador;
- cobertura diária;
- falhas de coleta;
- distribuições implausíveis;
- inconsistência entre título e campos;
- divergência entre snapshots.
---
## 14. Inspeção inicial do SQLite
Consultas somente leitura úteis:
### Tabelas e views
```sql
SELECT
type,
name,
tbl_name,
sql
FROM sqlite_master
ORDER BY type, name;
```
### Contagem aproximada de tabelas
Gerar consultas após listar os nomes. Não concatenar nomes externos sem validação.
### Colunas
```sql
PRAGMA table_info('nome_da_tabela');
```
### Chaves estrangeiras
```sql
PRAGMA foreign_key_list('nome_da_tabela');
```
### Índices
```sql
PRAGMA index_list('nome_da_tabela');
```
### Integridade
```sql
PRAGMA quick_check;
```
`quick_check` é somente leitura, mas pode ser custoso em bancos grandes. Executar quando apropriado.
---
## 15. Camada derivada recomendada
Sem alterar o banco original, criar:
```text
data/derived/
├── listings_clean.parquet
├── snapshots_clean.parquet
├── property_candidates.parquet
├── price_history.parquet
├── locations_normalized.parquet
└── quality_metrics.parquet
```
Ou um SQLite derivado:
```text
data/derived/analytics.sqlite
```
Toda derivação deve registrar:
- data;
- versão do código;
- fonte;
- filtros;
- hash ou identificação do banco de origem;
- contagem de entrada e saída.
---
## 16. Dicionário do esquema real
Após inspecionar o banco, produzir:
```text
docs/ACTUAL_SCHEMA.md
```
Esse arquivo deve conter:
- diagrama das tabelas;
- descrição de cada coluna;
- chaves;
- índices;
- cardinalidades;
- exemplos pequenos;
- problemas conhecidos;
- correspondência com este modelo conceitual;
- decisões de interpretação.
Não alterar `DATA_MODEL.md` para simplesmente espelhar um esquema ruim. Manter separação entre modelo conceitual e implementação real.

265
docs/PROJECT_CONTEXT.md

@ -0,0 +1,265 @@
# Contexto do projeto — Busca de Imóveis DF
## 1. Objetivo
Construir uma base histórica e uma metodologia confiável para apoiar a compra de um imóvel residencial no Distrito Federal.
O objetivo não é apenas encontrar o menor preço. A decisão deve equilibrar:
- segurança patrimonial de longo prazo;
- adequação funcional do imóvel;
- qualidade de vida;
- liquidez;
- custo total de aquisição e manutenção;
- risco de reformas;
- impacto financeiro mensal;
- fatores psicológicos associados a possuir uma residência estável.
A análise deve ser lenta, comparativa e baseada em evidências. Não existe urgência artificial para comprar.
---
## 2. Filosofia de decisão
### Princípios confirmados
- Evitar comprar apenas por medo de valorização futura.
- Evitar comprometer a reserva de emergência.
- Não aceitar grandes concessões de planta, conservação ou funcionalidade apenas para alcançar um preço nominal.
- Considerar reforma como custo financeiro, operacional e psicológico.
- Preferir imóvel pronto ou com intervenções pequenas e previsíveis.
- Comparar comprar com continuar alugando.
- Considerar que a compra será provavelmente em uma região diferente da moradia atual.
- Separar segurança emocional de justificativas financeiras.
- Reconhecer o valor psicológico de possuir um imóvel físico de longo prazo, sem transformar isso em justificativa para uma compra ruim.
- Tratar preço anunciado como ponto inicial de negociação, não como valor de mercado definitivo.
---
## 3. Situação residencial atual
Últimos valores consolidados no histórico do projeto:
- localização atual: Asa Sul, Brasília;
- imóvel alugado: aproximadamente 66 m²;
- aluguel: R$ 3.200 por mês;
- condomínio e IPTU não incluídos nesse valor;
- reajuste anual usado nas simulações anteriores: 4,5%.
Esses números devem ser confirmados antes de novas simulações, pois podem mudar.
A comparação aluguel versus compra deve considerar que o padrão, a localização e a tipologia dos imóveis candidatos podem ser diferentes do imóvel alugado atual.
---
## 4. Regiões prioritárias
### 4.1 Cruzeiro
O Cruzeiro, especialmente o Cruzeiro Novo, é um dos dois principais candidatos.
Aspectos relevantes:
- localização central;
- acesso ao Plano Piloto;
- blocos e quadras com diferenças importantes;
- estoque antigo, com variação de conservação;
- risco de imóveis que exigem reforma;
- diferenças de estacionamento, garagem, elevador, ruído e posição do bloco;
- apartamentos de dois e três quartos podem ter áreas totais semelhantes;
- preço deve ser analisado em conjunto com planta, estado e custos de adequação.
### 4.2 Jardins Mangueiral
Jardins Mangueiral é o outro principal candidato.
Aspectos relevantes:
- casas mais novas em comparação com parte do estoque do Cruzeiro;
- sensação de segurança, silêncio e residência de longo prazo;
- proximidade de São Sebastião, onde vivem a namorada do usuário e familiares dela;
- potencial de desenvolvimento da região em horizonte de 15 a 20 anos;
- impacto de trânsito parcialmente mitigado por flexibilidade de horário e trabalho remoto;
- logística compatível com o horário escolar da filha em parte dos dias;
- necessidade de avaliar ruído de vizinhança e qualidade das adaptações internas.
Somente casas de planta original de três quartos fazem parte do conjunto de candidatos.
---
## 5. Preferências funcionais
O imóvel ideal deve:
- funcionar como residência de longo prazo;
- exigir pouca ou nenhuma reforma relevante;
- ter planta coerente;
- evitar circulação desperdiçada;
- ter boa iluminação e ventilação;
- oferecer espaço social útil;
- evitar duplicações de cozinha ou área gourmet sem função clara;
- ter conservação compatível com o preço;
- minimizar custos escondidos;
- preservar flexibilidade para mudanças futuras da família.
No Jardins Mangueiral, o fechamento do quintal não é automaticamente negativo. A análise deve avaliar se a área foi convertida em espaço realmente útil, ventilado e integrado.
---
## 6. Referência financeira
### Último retrato consolidado
Valores informados no histórico, sujeitos a atualização:
- recursos líquidos: R$ 222.000;
- saldo de FGTS: R$ 130.000;
- reserva mínima desejada após a compra: R$ 50.000;
- capacidade de poupança: R$ 15.000 a cada seis meses;
- faixa de preço mais compatível com os imóveis desejados: aproximadamente R$ 650.000 a R$ 750.000;
- faixa anterior de referência: R$ 600.000.
Não use esses valores como atuais sem registrar a data da análise e pedir confirmação quando a conclusão depender deles.
### Regras financeiras
- FGTS não deve ser tratado como liquidez disponível para emergências.
- Custos de compra devem ser adicionados ao preço:
- ITBI;
- registro;
- escritura quando aplicável;
- avaliação;
- tarifas;
- mudança;
- reparos;
- mobiliário;
- eventual reforma;
- custos de oportunidade.
- A reserva mínima de R$ 50.000 não deve ser consumida pela entrada, custos ou reforma.
- A prestação não deve ser analisada isoladamente.
- Comparar:
- aluguel;
- condomínio;
- IPTU;
- seguros;
- manutenção;
- juros;
- amortização;
- custo de oportunidade da entrada;
- valorização;
- reajuste do aluguel;
- liquidez do imóvel.
- Distinguir patrimônio acumulado de fluxo de caixa mensal.
---
## 7. Financiamento e consórcio
### Financiamento
Simulações anteriores consideraram diferentes cenários de:
- SAC;
- prazos de 20, 30 e até 35 anos;
- juros próximos de 9% ao ano em alguns cenários;
- amortizações periódicas com FGTS;
- amortizações extraordinárias em dinheiro.
Toda nova simulação deve explicitar:
- taxa nominal e efetiva;
- indexador;
- CET;
- sistema de amortização;
- prazo;
- entrada;
- custos acessórios;
- uso de FGTS;
- estratégia de amortização;
- inflação;
- retorno líquido alternativo;
- horizonte de permanência.
### Pró-cotista
A estratégia não deve assumir que o Pró-cotista estará disponível. O usuário já percebeu que provavelmente não receberá essa linha.
### Consórcio
O consórcio do Banco do Brasil foi considerado como alternativa.
Informações recebidas do gerente, tratadas como relatos e não como garantias:
- grupos com desconto podem atrair participantes dispostos a oferecer lances elevados;
- agosto e setembro podem ser meses relativamente melhores para ofertar lance;
- outubro pode ficar mais competitivo por coincidir com pagamento de PLR de funcionários do BB;
- muitos participantes dos consórcios do BB seriam funcionários do próprio banco.
Essas afirmações precisam ser verificadas com dados do grupo sempre que forem usadas. Não extrapolar um grupo para todos os grupos.
A estratégia atual é não agir com pressa e observar os percentuais de lances ao longo do tempo.
---
## 8. Fatores familiares e logísticos
- O usuário tem uma filha em idade escolar.
- A namorada mora em São Sebastião e tem um filho.
- Jardins Mangueiral pode reduzir a distância para a namorada e a família dela.
- O usuário tem flexibilidade de trabalho e de horário em parte da semana.
- A logística familiar deve ser considerada, mas não deve justificar um imóvel inadequado.
- A compra deve continuar válida mesmo se circunstâncias relacionais mudarem.
---
## 9. Horizonte
A decisão deve ser avaliada como residência e patrimônio de longo prazo.
Questões centrais:
- O imóvel continuará funcional em 10, 15 ou 20 anos?
- A localização atende a rotina sem depender de uma única circunstância atual?
- O custo mensal permanece confortável em cenários adversos?
- Existe reserva para manutenção, saúde, carro e emergências?
- A planta permite adaptação?
- A revenda é razoável?
- A compra reduz ou aumenta a fragilidade financeira?
---
## 10. Questões em aberto
Manter estas questões explícitas até que os dados permitam respondê-las:
- valor atual disponível em dinheiro e FGTS;
- limite confortável de prestação;
- prazo máximo desejável;
- custo total aceitável de aquisição;
- peso relativo entre Cruzeiro e Jardins Mangueiral;
- blocos e quadras prioritários no Cruzeiro;
- QCs prioritárias no Mangueiral após visitas;
- tolerância a imóveis sem elevador ou garagem;
- impacto real do trânsito;
- custo de manutenção de casas versus apartamentos;
- tamanho mínimo funcional;
- horizonte provável de permanência;
- estratégia final entre financiamento, consórcio e espera.
---
## 11. Fonte de verdade
Este documento consolida preferências e premissas, mas não substitui uma confirmação recente quando a decisão depender de:
- saldo financeiro;
- renda;
- taxas bancárias;
- regras de FGTS;
- custos cartorários;
- disponibilidade de crédito;
- preços atuais;
- condições de consórcio;
- situação familiar ou logística.
Toda informação temporal deve carregar data de referência.

438
docs/PROPERTY_CRITERIA.md

@ -0,0 +1,438 @@
# Critérios de avaliação dos imóveis
## 1. Estrutura da avaliação
Classificar cada critério como:
- **eliminatório**: imóvel deve ser excluído;
- **forte**: pode inviabilizar a compra mesmo com bom preço;
- **moderado**: influencia comparação e negociação;
- **informativo**: deve ser registrado, mas não decide sozinho.
Não criar pesos numéricos arbitrários sem aprovação. Quando usar pontuação, mostrar os pesos, a justificativa e o efeito de cada critério.
---
## 2. Critérios eliminatórios gerais
Excluir ou colocar em quarentena analítica quando houver:
- documentação incompatível ou não esclarecida;
- área, tipologia ou localização materialmente divergentes;
- planta que não atenda às necessidades de longo prazo;
- preço total incompatível com a preservação da reserva;
- problemas graves de umidade, infiltração ou estrutura;
- ruído intolerável identificado em visita;
- custo oculto que destrua a vantagem aparente;
- anúncio sem informação mínima para comparação;
- indícios relevantes de fraude ou inconsistência.
Um imóvel não deve ser eliminado apenas por falta de dados no anúncio. Nesse caso, classifique-o como pendente de verificação.
---
## 3. Critérios gerais de qualidade
### 3.1 Planta e funcionalidade
Avaliar:
- distribuição dos cômodos;
- privacidade dos quartos;
- largura de circulação;
- áreas mortas;
- integração social;
- capacidade de mobiliamento;
- armazenamento;
- número e localização de banheiros;
- ventilação cruzada;
- iluminação natural;
- possibilidade de adaptação futura;
- conflito entre portas, móveis e circulação;
- uso real de varandas, quintais e áreas gourmet.
### 3.2 Estado de conservação
Registrar separadamente:
- estrutura;
- elétrica;
- hidráulica;
- revestimentos;
- esquadrias;
- telhado;
- impermeabilização;
- pintura;
- armários;
- climatização;
- equipamentos;
- sinais de reforma improvisada.
Não resumir tudo como “reformado”. Uma reforma estética pode esconder sistemas antigos.
### 3.3 Custos de adequação
Estimar:
- reparos imediatos;
- intervenções em até dois anos;
- mobiliário obrigatório;
- eletrodomésticos;
- mudança;
- custo de desocupação, quando aplicável;
- custo de tempo e transtorno;
- contingência de obra.
Aplicar margem de segurança para reformas. Não usar apenas o orçamento mais otimista.
### 3.4 Conforto ambiental
Avaliar em horários diferentes:
- incidência solar;
- calor;
- ventilação;
- iluminação;
- ruído;
- odores;
- privacidade;
- proximidade de vias;
- fluxo de pedestres;
- iluminação noturna;
- segurança percebida;
- drenagem e risco de alagamento.
### 3.5 Liquidez e revenda
Considerar:
- tipologia;
- padrão da região;
- facilidade de financiamento;
- regularidade documental;
- garagem;
- elevador;
- posição;
- conservação;
- faixa de preço;
- público comprador provável;
- excesso de personalização;
- limitações permanentes de planta.
---
## 4. Cruzeiro Novo
### 4.1 Pontos a registrar
- quadra;
- bloco;
- posição no bloco;
- andar;
- orientação solar;
- vista;
- proximidade de vias e comércio;
- ruído;
- elevador;
- garagem ou estacionamento;
- estado das áreas comuns;
- taxa de condomínio;
- obras previstas;
- idade e conservação do prédio;
- prumadas;
- elétrica;
- fachada;
- acessibilidade;
- área privativa e área total;
- número original de quartos;
- alterações de planta;
- ocupação atual;
- situação documental.
### 4.2 Dois versus três quartos
No Cruzeiro, apartamentos anunciados como dois ou três quartos podem ter áreas semelhantes.
A análise deve verificar:
- planta original;
- quarto revertido;
- dependência transformada;
- sala dividida;
- ventilação do cômodo adicional;
- dimensão real dos quartos;
- impacto na circulação;
- valor de revenda.
Não classificar automaticamente três quartos como superior se o terceiro cômodo for inadequado.
### 4.3 Elevador e garagem
Ausência de elevador ou garagem é uma concessão relevante, não um detalhe.
Registrar:
- andar;
- número de lances de escada;
- acessibilidade futura;
- facilidade de mudança;
- uso por idosos;
- disponibilidade real de estacionamento;
- segurança ao estacionar;
- regras do condomínio.
O efeito no preço deve ser analisado com imóveis comparáveis.
### 4.4 Reforma
Imóveis antigos podem exigir modernização invisível no anúncio.
Durante a visita, verificar:
- quadro elétrico;
- aterramento;
- tomadas;
- tubulações;
- registros;
- pressão de água;
- vazamentos;
- esquadrias;
- ruído entre unidades;
- estado da laje;
- gás;
- ventilação de banheiros;
- infiltrações em fachadas;
- histórico de obras do bloco.
---
## 5. Jardins Mangueiral
### 5.1 Filtro de tipologia
Somente considerar casas com **planta de três quartos**.
Casas originalmente de dois quartos devem ser excluídas, mesmo quando reformadas ou anunciadas como tendo três quartos.
A validação pode usar:
- planta original;
- posição da escada;
- dimensões;
- fachada;
- implantação;
- documentos;
- comparação com unidades padrão;
- fotografias.
### 5.2 Quintal fechado e ampliações
A perda do quintal original não é automaticamente um problema.
A conversão deve ser considerada positiva quando cria:
- sala ampliada;
- espaço de convivência;
- ambiente de refeições;
- escritório;
- área social ventilada;
- integração útil com a casa;
- armazenamento funcional.
Penalizar quando a conversão:
- duplica cozinha ou cooktop sem necessidade;
- cria circulação inútil;
- reduz ventilação e iluminação;
- aumenta calor;
- usa material de baixa qualidade;
- compromete impermeabilização;
- dificulta manutenção;
- cria ambiente sem função clara;
- prejudica privacidade;
- ocupa todo o espaço sem compensação funcional.
### 5.3 Área gourmet
Não atribuir valor automático ao rótulo “área gourmet”.
Avaliar:
- frequência provável de uso;
- integração com a cozinha;
- redundância de equipamentos;
- exaustão;
- ventilação;
- limpeza;
- circulação;
- capacidade de receber pessoas;
- qualidade da execução;
- manutenção.
Um segundo cooktop ao lado da cozinha principal pode ser redundante.
### 5.4 QCs e acesso a serviços
Relatos de moradores usados como referência anedótica:
- QCs 8 e 9: percebidas como mais fortes para acesso prático a comércio e serviços;
- QCs 6 e 7: alternativas consideradas;
- QCs 10 e 11: também consideradas boas.
Essas preferências não substituem verificação de campo.
Para cada QC, medir ou registrar:
- distância a comércio;
- acesso a mercado, farmácia e serviços;
- saída viária;
- tempo em horários relevantes;
- transporte;
- topografia;
- iluminação;
- tráfego interno;
- ruído;
- estacionamento;
- áreas comuns;
- manutenção do condomínio.
### 5.5 Ruído e vizinhança
Avaliar explicitamente:
- paredes compartilhadas;
- ruído de passos e móveis;
- música;
- festas;
- cães;
- crianças;
- áreas de convivência;
- quadras esportivas;
- comércio;
- portarias;
- vias internas;
- motos;
- coleta de lixo;
- obras;
- templos;
- escolas;
- bares.
Fazer visitas em mais de um horário quando possível, incluindo noite ou fim de semana.
Perguntar aos moradores sem depender apenas do corretor.
### 5.6 Reformas e regularidade
Verificar:
- fechamento de área externa;
- cobertura;
- drenagem;
- calhas;
- telhado;
- impermeabilização;
- ventilação;
- padrão elétrico;
- hidráulica;
- alterações estruturais;
- aprovação condominial;
- regularidade documental;
- impacto em seguro e financiamento.
---
## 6. Checklist de visita
### Antes da visita
- salvar anúncio e data;
- registrar preço;
- registrar condomínio e IPTU;
- conferir área e quartos;
- buscar histórico do anúncio;
- verificar anúncios duplicados;
- mapear entorno;
- listar dúvidas.
### Durante a visita
- fotografar com autorização;
- registrar ruído com janelas abertas e fechadas;
- testar torneiras, descargas, chuveiros e registros;
- observar umidade e odores;
- verificar tomadas e quadro;
- medir cômodos críticos;
- conferir incidência solar;
- observar circulação;
- avaliar armazenamento;
- verificar sinal de celular e internet;
- examinar garagem ou estacionamento;
- conversar com porteiro ou moradores quando apropriado;
- perguntar motivo da venda;
- perguntar tempo de anúncio;
- perguntar reformas e documentação;
- verificar taxas extraordinárias.
### Depois da visita
- registrar impressão no mesmo dia;
- separar fatos de percepções;
- estimar custos;
- comparar com alternativas reais;
- listar riscos;
- definir preço máximo;
- registrar pendências documentais;
- evitar decisão motivada apenas por entusiasmo.
---
## 7. Negociação
O preço máximo deve considerar:
- valor comparável;
- estado;
- custos de compra;
- reforma;
- liquidez;
- concessões;
- risco;
- tempo de anúncio;
- histórico de preço;
- condição de pagamento;
- custo de oportunidade.
Não negociar com base apenas no percentual de desconto.
O desconto nominal deve ser comparado ao custo real de corrigir as deficiências.
---
## 8. Saída padrão por imóvel
Cada ficha analítica deve conter:
- URL e identificador;
- data de coleta;
- região, quadra, bloco ou QC;
- tipologia original;
- área;
- quartos;
- garagem;
- elevador;
- preço;
- condomínio;
- IPTU;
- preço por m² válido ou não;
- histórico de preço;
- possíveis duplicatas;
- pontos fortes;
- pontos fracos;
- reformas;
- custo estimado de adequação;
- riscos;
- pendências;
- posição relativa aos comparáveis;
- preço máximo sugerido, quando houver dados;
- decisão: excluir, monitorar, visitar, negociar ou candidato forte.

22
headers.txt

@ -0,0 +1,22 @@
HTTP/2 403
date: Mon, 13 Jul 2026 12:29:34 GMT
content-type: text/html; charset=UTF-8
content-length: 5753
accept-ch: Sec-CH-UA-Bitness, Sec-CH-UA-Arch, Sec-CH-UA-Full-Version, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform, Sec-CH-UA, UA-Bitness, UA-Arch, UA-Full-Version, UA-Mobile, UA-Model, UA-Platform-Version, UA-Platform, UA
cf-mitigated: challenge
content-security-policy: default-src 'none'; script-src 'nonce-NDFRcU4DySSBatDK11iuTs' 'unsafe-eval' https://challenges.cloudflare.com; script-src-attr 'none'; style-src 'unsafe-inline'; img-src 'self' https://challenges.cloudflare.com; connect-src 'self' https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com blob:; child-src 'self' https://challenges.cloudflare.com blob:; worker-src blob:; form-action http: https:; base-uri 'self'
server: cloudflare
critical-ch: Sec-CH-UA-Bitness, Sec-CH-UA-Arch, Sec-CH-UA-Full-Version, Sec-CH-UA-Mobile, Sec-CH-UA-Model, Sec-CH-UA-Platform-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform, Sec-CH-UA, UA-Bitness, UA-Arch, UA-Full-Version, UA-Mobile, UA-Model, UA-Platform-Version, UA-Platform, UA
cross-origin-embedder-policy: require-corp
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
origin-agent-cluster: ?1
permissions-policy: accelerometer=(),camera=(),clipboard-read=(),clipboard-write=(),geolocation=(),gyroscope=(),hid=(),magnetometer=(),microphone=(),payment=(),publickey-credentials-get=(),screen-wake-lock=(),serial=(),sync-xhr=(),usb=(),xr-spatial-tracking=*
referrer-policy: same-origin
server-timing: chlray;desc="a1a84b426c72f17f"
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=RJh2QutRJQCG335sylJHf3W5WhCN3A6QHLF%2FYrIaRQuW0iICamlOrJMou8pb%2BZtSwNZ7YHwnR8mq2PmVZV8LhatfWgSaoMSttSNrqvk9gW5uhc7qag5fNebLVMYN7iMuRjh8b4z2"}]}
nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
cf-ray: a1a84b426c72f17f-GRU

8
house-quest.code-workspace

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

172
reports/cruzeiro_top10/README.md

@ -0,0 +1,172 @@
# Ranking preliminar — apartamentos no Cruzeiro Novo
**Data de referência do snapshot:** 17/07/2026
**Relatório revisado:** 17/07/2026
**Unidade principal:** imóvel físico provável, não snapshot nem anúncio isolado
**Finalidade:** priorizar visitas; não estimar preço negociado nem substituir vistoria
## Pergunta
Quais são os dez imóveis do Cruzeiro Novo que mais merecem atenção segundo as preferências e regras do projeto, dando às fotografias importância comparável à dos dados estruturados?
## Universo e método
Foram avaliados os 65 anúncios encontrados pela busca:
`https://www.dfimoveis.com.br/venda/df/cruzeiro/novo/apartamento/3-quartos`
A coleta cobre um único dia e um único portal. O banco foi aberto somente em leitura. Os campos foram complementados com os HTMLs brutos, pois localização e descrição no SQLite têm limitações já documentadas em `docs/ACTUAL_SCHEMA.md`.
Na coleta concluída em 17/07, os 65 anúncios do Cruzeiro permaneceram ativos e nenhum anúncio novo entrou. O único preço alterado foi o do anúncio `1372539`, de R$ 572.500 para R$ 569.000; ele continua fora do ranking porque a descrição confirma planta de três quartos convertida em dois. Assim, a ordem foi mantida após revisão das mudanças materiais.
Não foi criada pontuação numérica, pois os pesos não foram aprovados. A ordem é uma **prioridade de visita ajustada por risco**, aplicando:
1. critérios eliminatórios e inconsistências;
2. planta funcional de três quartos;
3. indícios de reforma relevante ou custos escondidos;
4. iluminação, ventilação, distribuição e conservação visíveis;
5. andar, elevador e garagem;
6. preço, área e condomínio anunciados;
7. qualidade e completude da evidência.
### Revisão fotográfica
Em 17/07/2026, a primeira foto dos dez escolhidos foi auditada especificamente para marcas de “vendido” ou “negócio fechado”. Nenhuma das dez apresentou esse sinal.
A primeira passagem examinou até 16 imagens de cada um dos 65 anúncios, totalizando 1.029 arquivos. Depois, foram examinadas todas as 532 fotos dos 17 finalistas. Descontada a sobreposição entre as etapas, 1.289 arquivos distintos foram efetivamente vistos.
As imagens foram usadas para observar conservação, coerência da reforma, luz, ventilação aparente, tamanho relativo dos quartos, circulação, integração social, cozinha, banheiros, área de serviço, fachada e estacionamento. Fotografias não permitem validar elétrica, hidráulica, infiltração, ruído, estrutura ou documentação.
Muitos anúncios incluem, depois das fotos do imóvel, imagens genéricas do portal, logos e até cartões de outros imóveis marcados como “vendido”. Esses ativos foram desconsiderados na avaliação visual do apartamento.
## Ranking
### 1. Anúncio 1345146 — SHCES 1207, bloco A
- **Anunciado:** R$ 670.000; 65 m²; condomínio R$ 605; 3 quartos, 1 suíte; 4º andar.
- **Por que lidera:** é o conjunto mais equilibrado entre reforma pronta, planta preservada e acessibilidade. O anúncio declara elevador, diferencial forte para uso de longo prazo.
- **Fotos:** reforma coerente e bem conservada; cozinha planejada, área de serviço separada, dois banheiros atualizados, três quartos reconhecíveis e boa luz. Os dormitórios e a área de serviço parecem compactos, mas funcionais.
- **Pendências:** confirmar funcionamento/obras do elevador, posição solar, área da matrícula, estacionamento/garagem e sistemas elétrico e hidráulico.
- **Ação:** candidato forte; visitar primeiro.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1207-bloco-a-1345146
### 2. Anúncio 1232283 — SHCES 309, bloco B
- **Anunciado:** R$ 600.000; 70 m²; condomínio R$ 497; 3 quartos, 1 suíte; 3º andar; 2 vagas; nascente e vazado.
- **Por que está no topo:** maior área entre os primeiros colocados, preço competitivo, duas vagas anunciadas e reforma recente. O anúncio informa vista livre e ocupação imediata.
- **Fotos:** ambientes claros, armários abundantes, dois banheiros modernos e reforma consistente. Um dos dormitórios parece estreito; a qualidade da circulação precisa ser sentida presencialmente.
- **Pendências:** confirmar se as duas vagas são privativas/vinculadas e se há elevador — ele não é informado. No 3º andar, sua ausência é concessão forte.
- **Ação:** candidato forte; visitar, condicionado à confirmação das vagas e acessibilidade.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-309-bloco-b-1232283
### 3. Anúncio 1357415 — SHCES 1109
- **Anunciado:** R$ 630.000; 67,2 m²; condomínio R$ 350; 3 quartos, 1 suíte; 4º andar; nascente e vazado.
- **Por que se destaca:** reforma visualmente mais homogênea que a maioria, bloco revitalizado, vista livre, boa ventilação e condomínio anunciado baixo.
- **Fotos:** sala social bem resolvida, cozinha planejada, marcenaria útil, dois banheiros atualizados, quartos claros e conservação convincente. Não aparecem sinais evidentes de obra imediata.
- **Pendências:** não há elevador informado e o imóvel está no 4º andar; confirmar isso antes da visita. Verificar estacionamento, escopo real da reforma e autenticidade do condomínio de R$ 350.
- **Ação:** candidato forte se a escada for aceitável; caso contrário, rebaixar fortemente.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1109-1357415
### 4. Anúncio 1357633 — SHCES 309
- **Anunciado:** R$ 600.000; 64 m²; 3 quartos; 2º andar; nascente, vazado e de canto.
- **Por que se destaca:** o anúncio declara reforma de elétrica, hidráulica, pisos e acabamentos, além de documentação regular. O 2º andar reduz o impacto provável da ausência de elevador.
- **Fotos:** acabamento moderno e uniforme, boa marcenaria, iluminação natural e três dormitórios aparentes. A planta permanece convencional e funcional; não há suíte anunciada, mas há banheiro de serviço.
- **Pendências:** condomínio, IPTU, garagem e elevador estão ausentes. Confirmar notas/escopo da reforma e ventilação dos banheiros.
- **Ação:** candidato forte; visitar.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-309-1357633
### 5. Anúncio 1376958 — SHCES 911, bloco B
- **Anunciado:** R$ 599.000; 62 m²; condomínio R$ 600; 3 quartos, 1 suíte; 2º andar; nascente e vazado.
- **Por que se destaca:** combina preço baixo no grupo reformado, andar razoável, suíte e ventilação cruzada declarada.
- **Fotos:** sala ampla para a metragem, cozinha e banheiros atualizados, boa vista e luz. O quarto com beliche aparenta ser pequeno, mas utilizável; a área de serviço é funcional, embora simples.
- **Pendências:** prédio sem elevador segundo a descrição e sem garagem informada. Confirmar área privativa, ruído da via visível e o que está incluído no condomínio.
- **Ação:** visitar.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-911-bloco-b-1376958
### 6. Anúncio 1345903 — SHCES 1303
- **Anunciado:** R$ 565.000; 64,5 m²; condomínio R$ 529; 3 quartos, 1 suíte; 1º andar; vazado.
- **Por que se destaca:** menor preço do top 10, acessibilidade melhor por estar no 1º andar, suíte e informação explícita de elétrica e esquadrias novas.
- **Fotos:** imóvel cuidado e aparentemente habitável sem obra imediata. Banheiros estão atualizados, mas cozinha, pisos e parte da marcenaria têm linguagem mais antiga. A planta parece íntegra e os três quartos são visíveis.
- **Pendências:** somente estacionamento público; confirmar ventilação, ruído do 1º andar, hidráulica e segurança das janelas.
- **Ação:** visitar com foco em custo real de modernização opcional.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1303-1345903
### 7. Anúncio 1339324 — SHCES 703
- **Anunciado:** R$ 665.000; 64 m²; condomínio R$ 390; 3 quartos, 1 suíte; 3º andar.
- **Por que se destaca:** reforma pronta, climatização, condomínio anunciado baixo e excelente aproveitamento de marcenaria.
- **Fotos:** uma das melhores apresentações: integração social coerente, cozinha funcional, dois banheiros bem executados, três quartos utilizáveis, boa iluminação e áreas comuns cuidadas. A circulação íntima parece estreita.
- **Pendências:** elevador, garagem, posição solar e escopo técnico da reforma não são informados. O 3º andar sem elevador seria concessão relevante.
- **Ação:** visitar após esclarecer acessibilidade e estacionamento.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-703-1339324
### 8. Imóvel provável dos anúncios 1370889 e 1375200 — SHCES 207
- **Anunciado:** R$ 695.000; 62 m²; condomínio entre R$ 435 e R$ 453; 3 quartos, 1 suíte. Um anúncio informa 2º andar.
- **Por que se destaca:** prédio reformado, planta social integrada, suíte e condição pronta para morar.
- **Fotos:** os dois anúncios mostram o mesmo apartamento com alta confiança visual. Sala/cozinha integradas funcionam bem, há área de serviço independente, dois banheiros atualizados e três dormitórios. A reforma parece consistente, embora bastante personalizada.
- **Pendências:** confirmar formalmente que os anúncios são da mesma unidade, andar, orientação solar, área, garagem e elevador. A duplicidade não deve inflar a oferta.
- **Ação:** visitar uma única vez após confirmar o anúncio primário.
- **URLs:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-207-1370889 e https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-207-1375200
### 9. Anúncio 1372268 — SHCES 305, bloco I
- **Anunciado:** R$ 595.000; 62,61 m²; condomínio R$ 460; 3 quartos; 1º andar; canto e vazado; 2 vagas.
- **Por que entra:** duas vagas anunciadas e 1º andar são diferenciais duráveis. O preço deixa margem maior para atualização que os reformados mais caros.
- **Fotos:** planta convencional, clara e aparentemente conservada; cozinha e banheiros são funcionais, porém mais antigos. Não há sinal visual de obra estrutural, mas a modernização seria maior do que nos primeiros colocados.
- **Pendências:** confirmar documentalmente as duas vagas, ventilação, elétrica, hidráulica e orçamento real de adequação.
- **Ação:** visitar como alternativa de valor; não assumir que o desconto cobre reforma sem orçamento.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-305-bloco-i-1372268
### 10. Anúncio 1373131 — SHCES 305
- **Anunciado:** R$ 725.000; 65 m²; condomínio R$ 420; 3 quartos; 2º andar; nascente e vazado; sem elevador.
- **Por que entra:** é a reforma tecnicamente mais bem descrita: elétrica e hidráulica renovadas, projeto de arquiteta, boa orientação e ventilação cruzada.
- **Fotos:** execução visual limpa e consistente, cozinha e banheiro novos, três quartos preservados e boa luminosidade. Não possui suíte anunciada e a linguagem totalmente branca deve ser avaliada sem o efeito de grande-angular.
- **Pendências:** não há garagem informada; preço é alto para unidade sem suíte, elevador ou vaga. Confirmar documentação da reforma e medidas reais dos ambientes.
- **Ação:** visitar somente se a condição pronta justificar o prêmio pedido.
- **URL:** https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-305-1373131
## Quase entraram
- **[1382466](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-101-bloco-c-1382466), R$ 650.000:** boa reforma aparente e uma vaga, mas faltam fotos da cozinha e informações de andar, elevador e orientação; aprofundar antes de substituir alguém do top 10.
- **[1290510](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-911-bloco-a-1290510), R$ 640.000:** três quartos com suíte, vazado e reformado, mas fica no 4º andar sem elevador informado e os valores de condomínio/IPTU foram claramente extraídos como R$ 1; precisa de confirmação.
- **[1371813](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1109-bloco-d-1371813), R$ 709.000:** reforma moderna e 68 m², mas o conjunto de fotos é curto e não esclarece bem três quartos, área de serviço, andar, garagem ou elevador.
## Quarentenas e exclusões relevantes
- **1373346:** é um imóvel no Noroeste capturado indevidamente pela busca; excluído.
- **1212351, 1338088, 1259968, 1315328 e 1372539:** planta de três quartos convertida em dois ou sala/escritório; não priorizados para residência de longo prazo.
- **1313649:** anunciado na descrição como dois quartos apesar do campo estruturado indicar três; condomínio de R$ 1.172; excluído até esclarecer planta.
- **1370861:** três quartos originais transformados em suíte master e escritório; não atende à funcionalidade desejada.
- **1365929, 1161400, 1278826 e similares:** fotos e descrição indicam reforma relevante, sem desconto claramente suficiente frente às alternativas prontas.
- **1366910, 1372290 e 1383534:** 22–35 fotos visualmente similares indicam o mesmo imóvel provável, mas os anúncios divergem entre SHCES 207 e SHCES 303 e entre 63 e 74 m². O conjunto seria competitivo, porém permanece em quarentena até confirmar endereço, área e anúncio válido.
- **1370948, 1371797 e 1372167:** grupo provável de anúncios duplicados, sem vantagem suficiente sobre o top 10.
- **1357834 e 1358242:** duplicata provável; há ainda um terceiro anúncio visualmente semelhante, 1382413.
- **1372907, 1348535 e 1380850:** elevador, garagem ou área podem ser atrativos, mas os preços de R$ 820 mil a R$ 990 mil não vêm acompanhados de vantagem funcional suficiente para superar as opções anteriores. A conclusão financeira depende de atualização dos saldos e limites do usuário.
Uma imagem com selo “vendido” foi tratada como sinal de inconsistência, não como prova de venda, porque muitos anúncios contêm ativos genéricos de outros imóveis.
## Limitações e verificações para visita
O ranking é sensível a cinco informações ainda não confirmadas: tolerância pessoal a escadas, existência jurídica das vagas, conceito correto de área, qualidade invisível das reformas e limite financeiro atual. Antes de qualquer proposta:
1. conferir matrícula, área privativa e vagas;
2. testar elétrica, hidráulica, pressão, esquadrias e sinais de infiltração;
3. medir os três quartos e validar que permanecem funcionais;
4. visitar em horário de tráfego e à noite para ruído e estacionamento;
5. verificar elevador, obras e atas do condomínio;
6. confirmar condomínio, IPTU, taxas extras e orientação solar;
7. comparar preço máximo somente depois de atualizar recursos, FGTS, reserva e prestação confortável.
## Reprodutibilidade
- `analysis.py`: extrai banco e HTMLs em leitura, produzindo TSV.
- `photo_similarity.py`: sinaliza duplicatas por hash visual simples; os resultados exigem revisão humana.
- `query.sql`: consultas principais de universo e qualidade.
- `parameters.json`: parâmetros e fingerprint do snapshot.
- `results.csv`: ranking em formato tabular.

BIN
reports/cruzeiro_top10/__pycache__/analysis.cpython-314.pyc

Binary file not shown.

BIN
reports/cruzeiro_top10/__pycache__/photo_similarity.cpython-314.pyc

Binary file not shown.

133
reports/cruzeiro_top10/analysis.py

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Read-only extraction helper for the Cruzeiro ranking.
The database is opened in a consistent read transaction so WAL content is
included. The script only prints a TSV and never updates the database.
"""
from __future__ import annotations
import gzip
import argparse
import re
import sqlite3
import sys
from pathlib import Path
from bs4 import BeautifulSoup
ROOT = Path(__file__).resolve().parents[2]
DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3"
DATA_ROOT = DB_PATH.parent
SEARCH_FRAGMENT = "/cruzeiro/novo/"
def clean(value: str | None) -> str:
return re.sub(r"\s+", " ", value or "").strip()
def sanitize_description(value: str) -> str:
value = re.sub(r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b", "[email]", value)
value = re.sub(
r"(?:\(\d{2}\)|\b\d{2}\b)\s*(?:\d[\s.-]*){5,11}\d",
"[telefone]",
value,
)
value = re.sub(r"\bCRECI\b.{0,30}", "", value, flags=re.IGNORECASE)
return clean(value)
def parse_html(relative_path: str) -> tuple[str, str, str]:
with gzip.open(DATA_ROOT / relative_path, "rt", encoding="utf-8", errors="replace") as fh:
soup = BeautifulSoup(fh.read(), "lxml")
description_node = soup.select_one("div.assined-imv")
description = ""
if description_node:
for node in description_node.select("span, a, button"):
node.decompose()
description = sanitize_description(description_node.get_text(" ", strip=True))
details: list[str] = []
for item in soup.select("ul.details-text li"):
text = clean(item.get_text(" ", strip=True))
if text and text not in details:
details.append(text)
address_node = soup.select_one('[itemprop="address"]')
address = clean(address_node.get_text(" ", strip=True) if address_node else "")
return description, " | ".join(details), address
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--ids-only", action="store_true")
args = parser.parse_args()
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
connection.execute("PRAGMA query_only = ON")
connection.execute("BEGIN")
rows = connection.execute(
"""
SELECT
l.listing_id, l.url, l.price_brl, l.condominium_brl, l.iptu_brl,
l.area_m2, l.bedrooms, l.suites, l.parking_spaces,
l.raw_html_path, COUNT(p.source_url) AS photo_count
FROM listings AS l
JOIN listing_searches AS s USING (listing_id)
LEFT JOIN photos AS p USING (listing_id)
WHERE instr(s.search_url, ?) > 0
AND l.inactive_at IS NULL
GROUP BY l.listing_id
ORDER BY l.price_brl, l.listing_id
LIMIT 100
""",
(SEARCH_FRAGMENT,),
).fetchall()
connection.close()
if args.ids_only:
for row in rows:
print(row[0])
return 0
print(
"listing_id\tprice_brl\tcondominium_brl\tiptu_brl\tarea_m2\tbedrooms\t"
"suites\tparking_spaces\tphoto_count\taddress\tdetails\tdescription\turl"
)
for row in rows:
(
listing_id,
url,
price,
condominium,
iptu,
area,
bedrooms,
suites,
parking,
raw_path,
photo_count,
) = row
description, details, address = parse_html(raw_path)
values = (
listing_id,
price,
condominium,
iptu,
area,
bedrooms,
suites,
parking,
photo_count,
address,
details,
description,
url,
)
print("\t".join("" if value is None else clean(str(value)) for value in values))
return 0
if __name__ == "__main__":
sys.exit(main())

22
reports/cruzeiro_top10/parameters.json

@ -0,0 +1,22 @@
{
"analysis_date": "2026-07-17",
"listing_data_reference_date": "2026-07-17",
"database_path": "dfimoveis_data/dfimoveis.sqlite3",
"database_main_file_sha256": "7a5102a62bc66a344bd9440fa36301d10a57711bcdd263b767b65e88f70957f4",
"logical_snapshot_sha256": "fe604aab8af7f67c833e6de2a0fc81c7dc2950ca440ad7b23ebd03d0c2546188",
"database_mode": "read_only_transaction_with_wal",
"search_url_fragment": "/cruzeiro/novo/",
"listing_limit": 100,
"observed_listing_count": 65,
"database_crawl_run_count": 16,
"active_listing_count": 65,
"new_listing_count_since_previous_report": 0,
"material_price_change_count": 1,
"ranking_unit": "probable_physical_property",
"numeric_score_used": false,
"photo_first_pass_limit_per_listing": 16,
"photo_finalist_count": 17,
"photo_files_reviewed_distinct": 1289,
"first_photo_sale_marker_audit_count": 10,
"visually_unavailable_listing_ids": []
}

87
reports/cruzeiro_top10/photo_similarity.py

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Suggest duplicate Cruzeiro ads from visually similar local photos.
Uses a simple difference hash as a screening signal. Results are candidates for
manual review, never automatic proof that two ads describe the same property.
"""
from __future__ import annotations
import sqlite3
from collections import defaultdict
from pathlib import Path
from PIL import Image, UnidentifiedImageError
ROOT = Path(__file__).resolve().parents[2]
DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3"
DATA_ROOT = DB_PATH.parent
def difference_hash(path: Path) -> int | None:
try:
with Image.open(path) as image:
pixels = list(image.convert("L").resize((9, 8)).getdata())
except (OSError, UnidentifiedImageError):
return None
value = 0
for row in range(8):
offset = row * 9
for column in range(8):
value = (value << 1) | (pixels[offset + column] > pixels[offset + column + 1])
return value
def main() -> None:
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
connection.execute("PRAGMA query_only = ON")
connection.execute("BEGIN")
rows = connection.execute(
"""
WITH cruzeiro AS (
SELECT s.listing_id
FROM listing_searches AS s
JOIN listings AS l USING (listing_id)
WHERE instr(search_url, '/cruzeiro/novo/') > 0
AND l.inactive_at IS NULL
), uncommon AS (
SELECT sha256
FROM photos
WHERE sha256 IS NOT NULL
GROUP BY sha256
HAVING COUNT(DISTINCT listing_id) <= 5
)
SELECT p.listing_id, p.local_path
FROM photos AS p
JOIN cruzeiro AS c USING (listing_id)
JOIN uncommon AS u USING (sha256)
WHERE p.local_path IS NOT NULL
ORDER BY p.listing_id, p.ordinal
"""
).fetchall()
connection.close()
hashes: dict[str, list[int]] = defaultdict(list)
for listing_id, relative_path in rows:
value = difference_hash(DATA_ROOT / relative_path)
if value is not None:
hashes[listing_id].append(value)
ids = sorted(hashes)
print("listing_a\tlisting_b\tvisually_similar_photos")
for index, first_id in enumerate(ids):
for second_id in ids[index + 1 :]:
available = list(hashes[second_id])
matches = 0
for first_hash in hashes[first_id]:
distances = [(first_hash ^ second_hash).bit_count() for second_hash in available]
if distances and min(distances) <= 6:
available.pop(distances.index(min(distances)))
matches += 1
if matches >= 3:
print(f"{first_id}\t{second_id}\t{matches}")
if __name__ == "__main__":
main()

48
reports/cruzeiro_top10/query.sql

@ -0,0 +1,48 @@
PRAGMA query_only = ON;
-- Universo do Cruzeiro Novo.
SELECT
l.listing_id,
l.url,
l.price_brl,
l.condominium_brl,
l.iptu_brl,
l.area_m2,
l.bedrooms,
l.suites,
l.parking_spaces,
l.first_seen_at,
l.last_seen_at,
l.raw_html_path
FROM listings AS l
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/cruzeiro/novo/') > 0
AND l.inactive_at IS NULL
ORDER BY l.price_brl, l.listing_id
LIMIT 100;
-- Cardinalidade de fotos por anúncio.
SELECT p.listing_id, COUNT(*) AS photo_count
FROM photos AS p
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/cruzeiro/novo/') > 0
AND EXISTS (
SELECT 1 FROM listings AS l
WHERE l.listing_id = p.listing_id AND l.inactive_at IS NULL
)
GROUP BY p.listing_id
ORDER BY p.listing_id
LIMIT 100;
-- Número de estados históricos por anúncio.
SELECT h.listing_id, COUNT(*) AS snapshot_count
FROM listing_history AS h
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/cruzeiro/novo/') > 0
AND EXISTS (
SELECT 1 FROM listings AS l
WHERE l.listing_id = h.listing_id AND l.inactive_at IS NULL
)
GROUP BY h.listing_id
ORDER BY h.listing_id
LIMIT 100;

11
reports/cruzeiro_top10/results.csv

@ -0,0 +1,11 @@
rank,representative_listing_id,probable_duplicate_ids,price_brl,area_m2,bedrooms,suites,floor,elevator,parking_spaces,decision
1,1345146,,670000,65,3,1,4,yes,,candidato forte
2,1232283,,600000,70,3,1,3,unknown,2,candidato forte
3,1357415,,630000,67.2,3,1,4,unknown,,candidato forte condicionado a escadas
4,1357633,,600000,64,3,0,2,unknown,,visitar
5,1376958,,599000,62,3,1,2,no,,visitar
6,1345903,,565000,64.5,3,1,1,unknown,0,visitar
7,1339324,,665000,64,3,1,3,unknown,,visitar após esclarecer acessibilidade
8,1370889,1375200,695000,62,3,1,2,unknown,,visitar uma única vez
9,1372268,,595000,62.61,3,0,1,unknown,2,visitar como alternativa de valor
10,1373131,,725000,65,3,0,2,no,,visitar se o prêmio pela reforma for aceitável

133
reports/cruzeiro_vs_mangueiral/README.md

@ -0,0 +1,133 @@
# Ranking reconstruído — Cruzeiro Novo x Jardins Mangueiral
**Data do snapshot:** 17/07/2026
**Unidade:** imóvel físico provável
**Finalidade:** priorizar investigação e visitas; não estimar preço de transação nem substituir vistoria
## Resumo executivo
O Jardins Mangueiral continua oferecendo os melhores candidatos por espaço, suíte, vagas e preço anunciado. O Cruzeiro Novo continua superior em centralidade, acesso ao Plano Piloto e menor dependência de carro.
A casa `1279892`, QC 10, permanece em primeiro lugar geral pelo equilíbrio entre R$ 595 mil, condição visual, três quartos prováveis e quintal convertido sem eliminação total da área aberta. O apartamento `1345146`, SHCES 1207, é o melhor do Cruzeiro e fica em segundo: reforma coerente, suíte e elevador anunciado, mas com preço maior e vaga ainda incerta.
Confiança do ranking: **moderada para priorização de visitas e baixa para decisão de compra**. Fotos e anúncios não confirmam documentação, estrutura, sistemas, ruído, conforto térmico ou preço negociável.
## Universo e atualização
- Fonte: DF Imóveis.
- Banco: 159 anúncios, 295 snapshots e 6.272 mídias.
- Cruzeiro Novo: 65 anúncios acumulados e ativos.
- Jardins Mangueiral: 94 anúncios acumulados; 84 ativos no banco e 10 inativos.
- Cobertura útil: coletas de 13 e 17/07/2026.
- Histórico: ainda curto demais para tendência, liquidez ou tempo real de mercado.
O banco foi aberto com `mode=ro`, `PRAGMA query_only = ON` e transação de leitura consistente para incluir o WAL. O `quick_check` retornou `ok`.
## Método de reconstrução
O comparativo foi refeito a partir dos rankings regionais corrigidos, preservando a ordem relativa dentro de cada região. Não foram usados preço por metro quadrado nem pontuação numérica, porque as áreas das casas e apartamentos não são semanticamente comparáveis e os pesos não foram aprovados.
A ordem geral combina:
1. funcionalidade de longo prazo e três quartos utilizáveis;
2. condição visual e risco de reforma;
3. iluminação, ventilação, circulação e armazenamento aparentes;
4. preço, custos conhecidos e margem para adequação;
5. acessibilidade, vagas e dependência de carro;
6. riscos específicos da tipologia;
7. qualidade e coerência da evidência fotográfica;
8. compatibilidade da localização com a rotina registrada no projeto.
No Cruzeiro, os principais ajustes de risco são escadas, ausência de garagem, sistemas antigos e obras do bloco. No Mangueiral, são planta original, regularização das ampliações, impermeabilização, calor/ventilação e logística.
## Auditoria de disponibilidade pelas fotos
A primeira foto dos 20 finalistas foi inspecionada especificamente para “vendido”, “negócio fechado” ou sinal equivalente.
- Nenhum dos dez finalistas do Cruzeiro apresentou esse sinal.
- No Mangueiral, quatro anúncios que apareceram em versões anteriores foram removidos e colocados em quarentena visual: `1346231`, `1349519`, `1351729` e `1366801`.
- O banco ainda os tratava como ativos; para um ranking de visitas, a evidência visual prevaleceu como sinal de indisponibilidade operacional.
- A marca não comprova registro, preço ou conclusão jurídica da transação.
## Ranking geral
| # | Região | Anúncio | Local | Preço | Motivo principal | Pendência decisiva |
|---:|---|---|---|---:|---|---|
| 1 | Mangueiral | [1279892](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-10-rua-f-1279892) | QC 10 | R$ 595.000 | Melhor equilíbrio entre preço, estado e espaço externo | Planta, áreas divergentes e regularização |
| 2 | Cruzeiro | [1345146](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1207-bloco-a-1345146) | SHCES 1207 A | R$ 670.000 | Melhor alternativa urbana; reformado e com elevador anunciado | Vaga, área da matrícula e sistemas |
| 3 | Mangueiral | [1350367](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-6-rua-c-1350367) | QC 6 | R$ 569.000 | Preço competitivo e ampliação social aparentemente funcional | Área, licenças, calor e drenagem |
| 4 | Cruzeiro | [1232283](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-309-bloco-b-1232283) | SHCES 309 B | R$ 600.000 | 70 m², suíte, nascente/vazado e duas vagas anunciadas | Elevador e natureza jurídica das vagas |
| 5 | Mangueiral | [1346017](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-10-1346017) | QC 10 | R$ 577.000 | Clara, pouco personalizada e com quintal majoritariamente aberto | Taxas, orientação e regularização da cobertura |
| 6 | Mangueiral | [1397378](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-8-rua-b-1397378) | QC 8 | R$ 600.000 | QC prática e implantação compatível com a planta de 68 m² | Manutenção, drenagem e galeria contaminada |
| 7 | Cruzeiro | [1357415](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1109-1357415) | SHCES 1109 | R$ 630.000 | Boa reforma, suíte e posição nascente/vazada | Confirmar elevador antes de agendar |
| 8 | Mangueiral | [1359960](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-7-1359960) | QC 7 | R$ 610.000 | Ampliação social flexível sem segunda cozinha completa evidente | Calor, ventilação e impermeabilização |
| 9 | Cruzeiro | [1357633](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-309-1357633) | SHCES 309 | R$ 600.000 | Reforma uniforme e sistemas declarados renovados | Elevador, garagem, condomínio e prova da reforma |
| 10 | Cruzeiro | [1376958](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-911-bloco-b-1376958) | SHCES 911 B | R$ 599.000 | Reformado, nascente, vazado e com suíte | Sem elevador e garagem não informada |
| 11 | Mangueiral | [1319996](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-14-1319996) | QC 14 | R$ 649.000 | Reforma completa, marcenaria e placas solares | QC, cobertura, taxas e homologação solar |
| 12 | Cruzeiro | [1345903](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1303-1345903) | SHCES 1303 | R$ 565.000 | Menor preço competitivo e primeiro andar | Sem garagem; ruído e segurança do térreo |
| 13 | Mangueiral | [1324014](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-12-rua-k-1324014) | QC 12 | R$ 649.000 | Acabamento interno e marcenaria fortes | Vaga, redundância gourmet e ventilação |
| 14 | Cruzeiro | [1339324](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-703-1339324) | SHCES 703 | R$ 665.000 | Integração social, suíte e boa apresentação visual | Terceiro andar, elevador e garagem desconhecidos |
| 15 | Mangueiral | [1333569](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-6-1333569) | QC 6 | R$ 650.000 | Ampliação social em uso e QC alternativa prática | Conceito dos 90 m² e manutenção da cobertura |
| 16 | Cruzeiro | [1370889](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-207-1370889) / [1375200](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-207-1375200) | SHCES 207 | R$ 695.000 | Apartamento reformado com suíte | Duplicata, andar, elevador, vaga e área |
| 17 | Mangueiral | [1359480](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-4-1359480) | QC 4 | R$ 535.000 | Referência de menor preço habitável | QC, planta, documentação e acabamento externo |
| 18 | Cruzeiro | [1372268](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-305-bloco-i-1372268) | SHCES 305 I | R$ 595.000 | Primeiro andar e duas vagas anunciadas | Provar vagas e orçar modernização |
| 19 | Mangueiral | [1356197](https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-4-1356197) | QC 4 | R$ 649.000 | Acabamento interno forte e casa vazia | Espaço coberto parece garagem; regularização |
| 20 | Cruzeiro | [1373131](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-305-1373131) | SHCES 305 | R$ 725.000 | Reforma tecnicamente bem descrita | Sem elevador, sem garagem e prêmio de preço |
## Comparação regional
| Dimensão | Cruzeiro Novo | Jardins Mangueiral |
|---|---|---|
| Faixa do top 10 | R$ 565 mil–R$ 725 mil | R$ 535 mil–R$ 650 mil |
| Mediana do top 10 | R$ 615 mil | R$ 610 mil |
| Espaço | 62–70 m²; conceito de área mais homogêneo | 68–90 m² informados; ampliações pouco comparáveis |
| Vagas | Frequentemente ausentes ou incertas | Uma ou duas na maioria dos finalistas |
| Acessibilidade | Depende fortemente de andar e elevador | Escada interna permanente em todos os sobrados padrão |
| Reforma | Elétrica, hidráulica e prédio antigos | Cobertura, drenagem, impermeabilização e ampliação |
| Mobilidade | Centralidade e menor dependência de carro | Maior dependência logística; proximidade de São Sebastião |
| Espaço social | Compacto | Quintal e ampliações mais flexíveis |
| Documentação crítica | Vaga, área, obras do bloco | Planta original e regularização das ampliações |
## Sensibilidade à prioridade pessoal
| Prioridade dominante | Região favorecida | Primeiro candidato |
|---|---|---|
| Centralidade e acesso ao Plano | Cruzeiro | 1345146 |
| Espaço, suíte, vagas e custo-benefício | Mangueiral | 1279892 |
| Menor preço habitável | Mangueiral | 1359480, com concessões |
| Apartamento próximo de R$ 600 mil | Cruzeiro | 1232283 |
| Menor risco de ampliação irregular | Cruzeiro | 1345146 ou 1357633 |
| Menor risco de sistemas prediais antigos | Mangueiral | 1346017 ou 1397378, sujeito a vistoria |
| Proximidade de São Sebastião e rede familiar | Mangueiral | 1279892 |
## Imóveis imediatamente fora do corte
- **Cruzeiro:** [1382466](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-101-bloco-c-1382466), [1290510](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-911-bloco-a-1290510) e [1371813](https://www.dfimoveis.com.br/imovel/apartamento-3-quartos-venda-novo-cruzeiro-df-shces-quadra-1109-bloco-d-1371813).
- **Mangueiral:** [1368562](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-5-1368562).
## Próxima ação recomendada
As quatro visitas mais informativas continuam sendo:
1. `1279892`, para testar a melhor hipótese de casa equilibrada;
2. `1345146`, para testar a melhor hipótese urbana com elevador;
3. `1350367`, para testar custo-benefício e ampliação no Mangueiral;
4. `1232283`, para testar apartamento maior, suíte e vagas anunciadas.
Registrar após cada visita: tempo real de deslocamento, sensação de espaço, ruído, estacionamento, escadas, ventilação, manutenção esperada e vontade de permanecer por 10–20 anos. Essas quatro visitas devem definir qual região recebe a rodada seguinte.
## Limitações
- Preços são pedidos, não transações.
- A cobertura histórica é curta e de um único portal.
- A situação financeira do projeto precisa ser confirmada antes de proposta ou preço máximo.
- Fotos não validam estrutura, sistemas, ruído, conforto térmico ou documentação.
- A planta original das casas e vagas/elevadores dos apartamentos continuam pendentes.
- A escolha regional ainda depende de uma preferência não consolidada: centralidade versus casa, espaço e proximidade familiar.
## Reprodutibilidade
- `results.csv`: ranking reconstruído.
- `parameters.json`: universo, método e fingerprint lógico do snapshot.
- `reports/cruzeiro_top10/`: fontes do ranking regional do Cruzeiro.
- `reports/mangueiral_top10/`: fontes do ranking regional do Mangueiral.

31
reports/cruzeiro_vs_mangueiral/parameters.json

@ -0,0 +1,31 @@
{
"analysis_date": "2026-07-17",
"listing_data_reference_date": "2026-07-17",
"database_path": "dfimoveis_data/dfimoveis.sqlite3",
"database_main_file_sha256": "046e0a2fef3eb3c2e5ebdf07d30bd04a31db0bb1ea315ccc536c6bf8ba96577e",
"logical_snapshot_sha256": "fe604aab8af7f67c833e6de2a0fc81c7dc2950ca440ad7b23ebd03d0c2546188",
"database_mode": "read_only_transaction_with_wal",
"source_reports": [
"reports/cruzeiro_top10/README.md",
"reports/mangueiral_top10/README.md"
],
"cruzeiro_listing_count": 65,
"mangueiral_listing_count": 94,
"mangueiral_active_listing_count": 84,
"mangueiral_inactive_listing_count": 10,
"total_listing_count": 159,
"database_snapshot_count": 295,
"database_photo_count": 6272,
"database_crawl_run_count": 16,
"cruzeiro_photo_files_reviewed_distinct": 1289,
"mangueiral_photo_files_reviewed_distinct_cumulative": 1907,
"total_photo_files_reviewed_distinct_cumulative": 3196,
"regional_finalists_compared": 20,
"ranking_merge_method": "preserve_regional_order_then_interleave_by_cross_typology_visit_priority",
"report_rebuilt_from_regional_results": true,
"ranking_unit": "probable_physical_property",
"numeric_score_used": false,
"cross_typology_price_per_m2_used": false,
"first_photo_sale_marker_audit_count": 20,
"visually_unavailable_listing_ids": ["1346231", "1349519", "1351729", "1366801"]
}

21
reports/cruzeiro_vs_mangueiral/results.csv

@ -0,0 +1,21 @@
rank,region,representative_listing_id,probable_duplicate_ids,location,price_brl,area_m2,bedrooms,suites,parking_spaces,key_condition,decision
1,mangueiral,1279892,,QC 10,595000,68,3,1,1,planta original e ampliações pendentes de documentos,candidato forte
2,cruzeiro,1345146,,SHCES 1207 bloco A,670000,65,3,1,,elevador anunciado e vaga pendente,candidato forte
3,mangueiral,1350367,,QC 6,569000,68,3,1,2,área ampliação e conforto térmico pendentes,candidato forte
4,cruzeiro,1232283,,SHCES 309 bloco B,600000,70,3,1,2,elevador e natureza das vagas pendentes,candidato forte condicionado
5,mangueiral,1346017,,QC 10,577000,68,3,1,2,taxas orientação e cobertura pendentes,candidato forte
6,mangueiral,1397378,,QC 8,600000,68,3,1,2,planta manutenção e cobertura pendentes,candidato forte
7,cruzeiro,1357415,,SHCES 1109,630000,67.2,3,1,,quarto andar e elevador desconhecido,confirmar elevador antes da visita
8,mangueiral,1359960,,QC 7,610000,68,3,1,2,conforto ambiental do quintal fechado pendente,visitar
9,cruzeiro,1357633,,SHCES 309,600000,64,3,0,,elevador garagem e condomínio pendentes,visitar
10,cruzeiro,1376958,,SHCES 911 bloco B,599000,62,3,1,,sem elevador e garagem não informada,visitar
11,mangueiral,1319996,,QC 14,649000,68,3,1,2,localização cobertura e placas solares pendentes,visitar
12,cruzeiro,1345903,,SHCES 1303,565000,64.5,3,1,0,primeiro andar e somente estacionamento público,alternativa de valor
13,mangueiral,1324014,,QC 12,649000,68,3,1,,vaga conforto da área gourmet e regularização pendentes,visitar
14,cruzeiro,1339324,,SHCES 703,665000,64,3,1,,terceiro andar elevador e garagem desconhecidos,esclarecer acessibilidade
15,mangueiral,1333569,,QC 6,650000,90,3,1,2,conceito de área e cobertura pendentes,visitar após os anteriores
16,cruzeiro,1370889,1375200,SHCES 207,695000,62,3,1,,duplicata provável elevador e garagem desconhecidos,confirmar anúncio primário
17,mangueiral,1359480,,QC 4,535000,70,3,1,2,planta documentação e acabamento externo pendentes,referência de valor
18,cruzeiro,1372268,,SHCES 305 bloco I,595000,62.61,3,0,2,vagas e custo de modernização pendentes,alternativa de valor
19,mangueiral,1356197,,QC 4,649000,68,3,1,2,uso do espaço coberto e regularização pendentes,visitar após os anteriores
20,cruzeiro,1373131,,SHCES 305,725000,65,3,0,,sem elevador e garagem não informada,visitar apenas se reforma justificar prêmio

303
reports/database_assessment.md

@ -0,0 +1,303 @@
# Avaliação inicial do banco de dados
**Data da avaliação:** 13 de julho de 2026
**Banco:** `dfimoveis_data/dfimoveis.sqlite3`
**Escopo:** diagnóstico estrutural e de qualidade anterior a qualquer análise dos imóveis
## 1. Pergunta respondida
O banco já representa de forma segura anúncios, snapshots históricos e imóveis físicos, com cobertura e qualidade suficientes para iniciar análises imobiliárias?
**Resposta curta:** ele representa anúncios e estados de conteúdo, mas ainda não representa imóveis físicos deduplicados. A coleta cobre apenas um dia e contém exatamente um snapshot por anúncio. Preços, áreas e quartos podem ser usados somente após validações específicas; localização e dados de anunciante estão fortemente contaminados e não devem ser usados como estão. Portanto, o banco ainda não sustenta tendência histórica, tempo de mercado, preço por m² ou comparáveis por localização.
## 2. Segurança e método
A análise foi feita exclusivamente com consultas agregadas ou amostras pequenas. O banco foi aberto com `sqlite3 -readonly` ou URI `mode=ro&immutable=1`, sempre com `PRAGMA query_only = ON`. Nenhum scraper foi executado e nenhuma instrução SQL de escrita foi enviada ao banco.
Identificação do arquivo analisado:
- tamanho: 5.017.600 bytes;
- modificação: `2026-07-13 14:20:34.601285975 -03:00`;
- SHA-256 inicial: `1ce3128b488c50c993d7328ac6e24b5905f083a7e04e3f636407eea1edd02b9d`;
- sem arquivos `-wal` ou `-shm` e sem processo com o arquivo aberto na inspeção inicial;
- `PRAGMA quick_check`: `ok`;
- `PRAGMA foreign_key_check`: nenhuma violação.
Na validação final, o arquivo principal continuava com o mesmo tamanho, `mtime` e SHA-256. Como seu modo persistido é WAL, o cliente SQLite havia criado dois sidecars de runtime: `dfimoveis.sqlite3-shm`, com 32.768 bytes, e `dfimoveis.sqlite3-wal`, vazio. Eles não foram removidos, conforme a regra explícita do projeto.
O esquema completo e as decisões de interpretação estão em `docs/ACTUAL_SCHEMA.md`.
## 3. Universo, período e cobertura
Não houve filtro ou exclusão de registros para este diagnóstico. O universo físico contém:
| Unidade | Quantidade |
|---|---:|
| Execuções do coletor | 4 |
| Anúncios únicos por `listing_id` | 156 |
| URLs de anúncio únicas | 156 |
| Snapshots de conteúdo | 156 |
| Vínculos anúncio–busca | 156 |
| Fotos registradas | 5.386 |
| Anúncios com pelo menos uma foto | 156 |
### Cobertura temporal
Todas as datas técnicas válidas estão em ISO 8601. A coleta cobre somente **13/07/2026**:
| Evento | UTC | Horário de Brasília (UTC−3) |
|---|---|---|
| Primeira execução iniciada | 12:20:58 | 09:20:58 |
| Primeira observação de anúncio | 12:56:32 | 09:56:32 |
| Última observação de anúncio | 13:25:51 | 10:25:51 |
| Última execução concluída | 17:20:34 | 14:20:34 |
As duas primeiras execuções terminaram com `status = ok`, mas registraram zero páginas, URLs e detalhes. As duas execuções com coleta efetiva somaram:
- 9 páginas de busca;
- 156 URLs encontradas;
- 156 detalhes processados com sucesso e zero falhas;
- 5.383 fotos baixadas e 3 falhas;
- zero anúncios marcados como inativos.
As buscas cobertas foram:
1. `https://www.dfimoveis.com.br/venda/df/cruzeiro/novo/apartamento/3-quartos` — 65 anúncios;
2. `https://www.dfimoveis.com.br/venda/df/brasilia/jardins-mangueiral/casa/3,4-quartos` — 91 anúncios.
O universo é, portanto, intencionalmente limitado a um portal e a duas consultas. A busca do Jardins Mangueiral aceita três **ou quatro** quartos: 89 casas foram extraídas com três quartos anunciados e 2 com quatro. Mesmo os 89 registros de três quartos não comprovam a planta original exigida pelo projeto.
## 4. Como as entidades são representadas
### Anúncio
`listings` guarda uma linha por `listing_id` do DF Imóveis, com URL única e o estado corrente. Os 156 IDs, URLs e hashes correntes são distintos. Essa é uma identidade de publicação no portal, não de unidade residencial.
### Snapshot ou observação histórica
`listing_history` guarda uma linha por par único `(listing_id, content_hash)`. Isso é um histórico de **mudanças de conteúdo**: uma nova observação idêntica não cria novo snapshot. `listings.first_seen_at` e `last_seen_at` condensam a janela observada.
No snapshot atual:
- todos os 156 anúncios têm exatamente um registro histórico;
- o hash histórico é igual ao hash corrente em todos os casos;
- `captured_at = first_seen_at` em todos os casos;
- `first_seen_at = last_seen_at` em todos os casos.
Assim, não há série histórica real ainda. Não é possível medir mudança de preço, republicação, duração, saída ou retorno do estoque.
### Execução do scraper
`crawl_runs` registra horários, estado e contadores JSON, mas não existe `run_id` em `listings`, `listing_history` ou `listing_searches`. A execução geradora só pode ser inferida por horário. Isso limita auditoria, cobertura por execução e diagnóstico de ausências.
### Imóvel físico
Não existe tabela ou chave para imóvel físico, unidade, endereço normalizado ou relação de duplicidade. Anúncios de corretores diferentes permanecem independentes. A base também não contém hashes perceptuais; possui apenas SHA-256 exato das imagens.
Logo, as unidades válidas hoje são:
- anúncio único: suportado por `listing_id`;
- estado alterado do anúncio: suportado por `listing_history`;
- imóvel físico provável/confirmado: **não suportado sem derivação e revisão**.
## 5. Qualidade dos dados
### 5.1 Integridade estrutural
Pontos positivos:
- arquivo íntegro segundo `quick_check`;
- nenhuma FK órfã em histórico, buscas ou fotos;
- todos os JSONs são sintaticamente válidos;
- todos os IDs de anúncio têm 6–7 caracteres numéricos;
- todas as URLs pertencem ao domínio e padrão esperados;
- todos os 156 HTMLs brutos referenciados existem;
- todos os hashes de conteúdo têm 64 caracteres;
- não há URL, hash corrente ou caminho de HTML repetido.
Limitações estruturais:
- nenhuma relação entre execução e item;
- fonte implícita, sem `source_id`;
- área armazenada em um único conceito;
- mídia ligada ao anúncio, não ao snapshot;
- ausência de entidade de imóvel físico e de localização normalizada;
- apenas autoíndices; não há índices por datas ou atributos analíticos.
### 5.2 Completude dos principais campos
| Campo | Ausentes | Percentual |
|---|---:|---:|
| preço | 0 | 0,0% |
| área | 0 | 0,0% |
| quartos | 0 | 0,0% |
| condomínio | 59 | 37,8% |
| IPTU | 144 | 92,3% |
| suítes | 41 | 26,3% |
| vagas | 57 | 36,5% |
| cidade | 156 | 100,0% |
| UF | 156 | 100,0% |
| nome do anunciante | 156 | 100,0% |
| latitude/longitude | 156 / 156 | 100,0% / 100,0% |
| data de publicação | 156 | 100,0% |
| data de inativação | 156 | 100,0% |
Esses percentuais medem apenas nulos. Para campos textuais, a taxa de preenchimento é enganosa porque o parser capturou conteúdo da página em vez do valor pretendido.
### 5.3 Contaminação de texto
Os problemas mais graves são:
- 146 endereços (93,6%) têm mais de 300 caracteres;
- 154 bairros (98,7%) têm mais de 100 caracteres;
- os dois valores curtos de bairro são `.` e um trecho de descrição, também inválidos;
- 98 códigos de anunciante (62,8%) têm mais de 100 caracteres;
- 153 valores de CRECI (98,1%) têm mais de 100 caracteres.
Os textos longos incluem navegação, formulários, política de privacidade e outros fragmentos da página. Eles podem conter dados de contato exibidos pelo portal; por isso, não foram reproduzidos neste relatório. Esses campos devem ser considerados **indisponíveis**, não apenas ruidosos.
Os títulos têm somente quatro valores distintos e funcionam como títulos genéricos por tipologia. As 156 descrições são distintas, mas têm apenas 156–161 caracteres, compatíveis com resumos/metadados truncados, não com a descrição completa. `extra_json` contém um array `json_ld` vazio nos 156 registros e não resolve a reextração.
### 5.4 Valores estruturados e outliers
| Tipo anunciado | N | Preço mínimo | Preço máximo | Área mínima | Área máxima | Quartos |
|---|---:|---:|---:|---:|---:|---:|
| Apartamento | 65 | R$ 480.000 | R$ 2.030.598 | 61 m² | 113 m² | 3 |
| Casa | 91 | R$ 450.000 | R$ 570.000.000 | 0,11 m² | 131,46 m² | 3–4 |
Não há valores nulos ou não positivos em preço e área, mas isso não significa validade:
- anúncio `1351663`: R$ 570.000.000, forte indício de erro de escala ou parsing;
- anúncio `1370918`: 0,11 m², área fisicamente implausível.
Não foi aplicado corte nem correção silenciosa. Também não se calculou preço por m², pois `area_m2` não distingue área útil, privativa, construída, total ou terreno.
### 5.5 Fotos e arquivos brutos
Das 5.386 linhas em `photos`:
- 5.383 têm arquivo local, e o tamanho em disco coincide com `bytes`;
- 3 não têm hash, MIME, tamanho ou caminho, correspondendo às três falhas registradas pelo coletor;
- há 4.514 WebP, 558 JPEG, 311 PNG e 3 sem MIME;
- os anúncios possuem de 5 a 61 fotos;
- não há caminho local nem ordinal duplicado dentro de anúncio.
Há 3.307 hashes distintos. Cento e quatorze hashes aparecem em mais de um anúncio, e um deles aparece nos 156 anúncios. Isso revela logos, selos, placeholders ou outros ativos comuns. Hash idêntico pode ser sinal de anúncio duplicado somente depois de excluir ativos genéricos e combinar outros sinais.
## 6. Duplicidades e imóveis físicos prováveis
Não há duplicidade técnica imediata por `listing_id`, URL ou `content_hash`. Uma assinatura exploratória composta por bairro, tipo, preço, área, quartos e vagas encontrou 13 grupos, equivalentes a 18 linhas excedentes. Esse resultado **não é evidência de 18 imóveis duplicados**, porque:
- o bairro está contaminado;
- preço, área e quartos podem coincidir entre unidades distintas;
- fotos compartilhadas incluem ativos genéricos;
- não há endereço/unidade confiável nem telefone normalizado;
- SHA-256 só detecta arquivos idênticos, não versões redimensionadas da mesma foto.
Consequentemente, o método de deduplicação desta avaliação foi apenas técnico: unicidade de anúncio por `listing_id`. Nenhuma fusão de anúncios nem contagem de imóveis físicos foi produzida.
Uma futura camada derivada deve combinar, com confiança e revisão manual:
- endereço/QC/quadra/bloco/unidade reextraídos;
- imagens após remoção de ativos comuns, idealmente com hash perceptual;
- preço, área, planta, quartos e vagas validados;
- anunciante e descrição normalizados;
- estados `unreviewed`, `probable`, `confirmed`, `rejected` e `ambiguous`.
## 7. Possíveis quebras de coleta
1. **Localização:** `address` e `neighborhood` capturaram grandes trechos da página; cidade, UF e coordenadas ficaram vazias.
2. **Anunciante:** `advertiser_code` e `creci` frequentemente capturaram texto extenso; `advertiser_name` ficou vazio.
3. **Dados estruturados:** o `json_ld` foi preservado como array vazio em todos os anúncios.
4. **Escala numérica:** pelo menos um preço e uma área são claramente suspeitos.
5. **Fotos:** três falhas foram registradas de modo consistente, sem arquivo local.
6. **Execuções vazias:** duas execuções `ok` não coletaram páginas. Sem motivo/configuração persistidos, não é possível diferenciar teste, filtro vazio ou falha silenciosa.
7. **Filtro do Mangueiral:** a URL coletada inclui casas de quatro quartos e não identifica planta original.
## 8. Limitações para qualquer conclusão imobiliária
- Um único dia não permite análise histórica ou tendência.
- A amostra cobre apenas DF Imóveis e duas buscas específicas.
- Preço anunciado não é preço negociado.
- Não há transações, status comprovado de venda ou evidência de fechamento.
- Não há imóveis físicos deduplicados.
- Não há localização analítica confiável por QC, quadra ou bloco.
- Não há conceito comparável de área.
- O estado de reforma, planta original, elevador, garagem, andar e qualidade funcional não está modelado de forma suficiente.
- Para Jardins Mangueiral, `bedrooms = 3` não valida planta original de três quartos.
Portanto, ainda não é apropriado produzir medianas regionais, comparáveis, preço por m², tempo de anúncio, liquidez ou recomendações de visita com base apenas neste banco.
## 9. Próximas ações recomendadas
Antes da análise dos imóveis:
1. corrigir e testar a extração de endereço, bairro, anunciante e CRECI usando os HTMLs já salvos, sem recrawling;
2. criar uma derivação reproduzível fora do banco original e preservar os valores brutos;
3. separar áreas privativa, útil, construída, total e de terreno, deixando nulo o conceito não comprovado;
4. criar regras explícitas de validação para preço e área, mantendo os outliers identificados em quarentena;
5. filtrar o Mangueiral por planta original de três quartos, não apenas pelo número anunciado;
6. coletar múltiplas datas e auditar a cobertura por busca antes de estimar permanência, entradas ou saídas;
7. numa evolução aprovada do coletor, adicionar `source_id`, `run_id` nos snapshots, referência ao HTML bruto histórico e versão/configuração da coleta;
8. construir `property_candidates` apenas em uma camada derivada, com confiança e revisão, sem fundir registros no banco original.
## 10. Consultas de reprodução
Todas devem ser executadas com `sqlite3 -readonly dfimoveis_data/dfimoveis.sqlite3` ou conexão Python URI em `mode=ro` e `PRAGMA query_only = ON`.
```sql
-- Objetos do esquema
SELECT type, name, tbl_name, sql
FROM sqlite_master
ORDER BY type, name;
-- Contagens (executar uma tabela por vez após validar os nomes)
SELECT COUNT(*) FROM crawl_runs;
SELECT COUNT(*) FROM listings;
SELECT COUNT(*) FROM listing_history;
SELECT COUNT(*) FROM listing_searches;
SELECT COUNT(*) FROM photos;
-- Cobertura
SELECT MIN(started_at), MAX(finished_at), COUNT(*)
FROM crawl_runs;
SELECT MIN(first_seen_at), MAX(last_seen_at),
COUNT(DISTINCT date(first_seen_at))
FROM listings;
-- Número de snapshots por anúncio
SELECT snapshots, COUNT(*) AS listings
FROM (
SELECT listing_id, COUNT(*) AS snapshots
FROM listing_history
GROUP BY listing_id
)
GROUP BY snapshots;
-- Completude de campos críticos
SELECT
COUNT(*) AS total,
SUM(price_brl IS NULL) AS missing_price,
SUM(area_m2 IS NULL) AS missing_area,
SUM(condominium_brl IS NULL) AS missing_condominium,
SUM(iptu_brl IS NULL) AS missing_iptu,
SUM(latitude IS NULL OR longitude IS NULL) AS missing_coordinates
FROM listings;
-- Contaminação textual sem imprimir os textos
SELECT
SUM(length(address) > 300) AS long_address,
SUM(length(neighborhood) > 100) AS long_neighborhood,
SUM(length(advertiser_code) > 100) AS long_advertiser_code,
SUM(length(creci) > 100) AS long_creci
FROM listings;
-- Integridade referencial e física
PRAGMA quick_check;
PRAGMA foreign_key_check;
```
## 11. Implicação para a decisão de compra
Esta avaliação reduz o risco de transformar uma coleta tecnicamente bem-sucedida em falsa precisão de mercado. O próximo ganho real não virá de comparar preços imediatamente, mas de corrigir localização e semântica de área, acumular histórico e construir uma deduplicação reversível. Até lá, os registros são úteis como inventário bruto de anúncios e arquivos, não como avaliação consolidada de imóveis físicos.

184
reports/mangueiral_top10/README.md

@ -0,0 +1,184 @@
# Ranking preliminar — casas no Jardins Mangueiral
**Data de referência do snapshot:** 17/07/2026
**Relatório revisado:** 17/07/2026
**Unidade principal:** imóvel físico provável, não snapshot nem anúncio isolado
**Finalidade:** priorizar visitas; não estimar preço negociado nem substituir vistoria ou validação documental
## Pergunta
Quais são as dez casas do Jardins Mangueiral que mais merecem atenção segundo as preferências e regras do projeto, dando às fotografias importância comparável à dos dados estruturados?
## Universo e método
Foram avaliados os 94 anúncios acumulados encontrados pela busca de casas no Jardins Mangueiral: 84 estavam ativos e 10 inativos no snapshot consistente de 17/07/2026. A base cobre um único portal. Há 159 anúncios nas duas regiões, 295 snapshots de conteúdo e 6.272 mídias; 133 anúncios têm dois snapshots, mas quatro mudanças foram materialmente relevantes nos campos imobiliários e apenas uma delas foi preço.
Como o banco estava em WAL, ele foi aberto com `mode=ro`, sem `immutable=1`, dentro de uma transação de leitura consistente e com `PRAGMA query_only = ON`. Entre os anúncios ativos, 80 informam três quartos e preço plausível entre R$ 450 mil e R$ 1,15 milhão. O anúncio de R$ 1,15 milhão descreve outra implantação, no condomínio American Garden, e não a casa-padrão de 68 m² das QCs; não é comparável ao universo prioritário. Esses números descrevem anúncios, não imóveis únicos nem preços negociados.
Não foi calculado preço por metro quadrado: os campos e descrições misturam área privativa, construída, total e de terreno. Tampouco foi criada pontuação numérica, porque os pesos não foram aprovados. A ordem é uma **prioridade de visita ajustada por risco**, considerando em conjunto:
1. compatibilidade provável com a planta original de três quartos;
2. funcionalidade, circulação, armazenamento e flexibilidade de longo prazo;
3. qualidade real da conversão do quintal, sem penalizar fechamento automaticamente;
4. luz e ventilação aparentes, conservação e coerência dos acabamentos;
5. risco e transtorno de adequação;
6. preço, condomínio, IPTU, vagas e área anunciados;
7. QCs citadas nos relatos de moradores, apenas como evidência anedótica;
8. completude, coerência e confiabilidade das fotos.
### Filtro da planta original
Os dez imóveis abaixo têm fachada, posição de escada, distribuição e dormitórios fotografados compatíveis com a tipologia padrão de três quartos, e nenhum deles é descrito como conversão de dois para três quartos. Isso sustenta apenas a classificação **planta original provável**. A confirmação exige planta, matrícula ou documentação da unidade. O anúncio 1374012, por exemplo, foi retirado da disputa apesar do preço baixo porque a própria descrição informa “3 quartos sendo 1 adaptado”.
### Revisão fotográfica
Em 17/07/2026, a primeira foto de todos os escolhidos foi auditada especificamente para marcas de “vendido” ou “negócio fechado”. Quatro anúncios que chegaram a integrar versões do ranking foram removidos por esse critério: `1346231`, `1349519`, `1351729` e `1366801`.
A revisão anterior havia examinado 1.892 arquivos distintos. Nesta atualização foram revisadas as imagens iniciais dos novos anúncios elegíveis e, em detalhe, 15 imagens coerentes do novo finalista `1397378`; imagens posteriores da galeria que pertencem visualmente a outros imóveis foram descartadas como contaminação. As plantas oficiais de duas e três unidades de três quartos e os mapas locais em `resources/` também foram confrontados com a galeria e a QC.
As fotos foram usadas para avaliar coerência da planta, presença dos três dormitórios, circulação, luz, ventilação aparente, conservação, cozinha, banheiros, lavanderia, quintal, integração social e qualidade das ampliações. Elas não validam elétrica, hidráulica, impermeabilização, estrutura, calor, ruído ou regularização.
As galerias baixadas frequentemente continuam, depois das imagens do imóvel, com recomendações do portal, fotos de outras casas, selos de “vendido”, publicidade e logos. Esses arquivos foram vistos, identificados pela quebra de coerência visual e desconsiderados. Por isso, quantidade bruta de fotos não foi tratada como qualidade de evidência, e hashes compartilhados foram usados apenas para triagem manual de duplicidade.
## Ranking
### 1. Anúncio 1279892 — QC 10
- **Anunciado:** R$ 595.000; 68 m² no campo estruturado e 107 m² na descrição; condomínio R$ 383; IPTU R$ 1.050; 3 quartos, 1 suíte; 1 vaga informada no banco.
- **Por que lidera:** oferece o melhor equilíbrio entre preço, localização considerada boa nos relatos, reforma pronta e planta visualmente coerente. A diferença para casas mais caras parece estar mais na ausência de mobiliário do que em obra obrigatória.
- **Fotos:** ambientes claros, sala ampla, cozinha planejada bem executada, banheiros atualizados e três dormitórios reconhecíveis. O fundo combina área coberta e trechos abertos, preservando luz e ventilação; não aparece uma segunda cozinha completa redundante.
- **Pendências:** esclarecer os conceitos de 68 e 107 m², a DCE citada, regularização das ampliações, número jurídico de vagas e impermeabilização da cobertura translúcida.
- **Ação:** candidato forte; visitar primeiro.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-10-rua-f-1279892
### 2. Anúncio 1350367 — QC 6
- **Anunciado:** R$ 569.000; 68 m² no banco, 107 m² totais e terreno de 131 m² na descrição; condomínio R$ 320; 3 quartos, 1 suíte; 2 vagas.
- **Por que está no topo:** preço baixo para o grupo final, QC considerada alternativa prática e ampliação social que parece efetivamente utilizável.
- **Fotos:** casa vazia, clara e aparentemente reformada; banheiros e pisos coerentes; três dormitórios aparentes. O quintal foi convertido em uma varanda ampla com churrasqueira e bancada, mas mantém abertura lateral e circulação, em vez de virar cômodo escuro ou segunda cozinha fechada.
- **Pendências:** confirmar área construída, licenças, ventilação e calor sob a cobertura, drenagem, elétrica da ampliação e o que falta de marcenaria/mobiliário.
- **Ação:** candidato forte; visitar.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-6-rua-c-1350367
### 3. Anúncio 1346017 — QC 10
- **Anunciado:** R$ 577.000; 68 m²; condomínio e IPTU ausentes; 3 quartos, 1 suíte; 2 vagas.
- **Por que se destaca:** casa pronta, luminosa e pouco personalizada por preço competitivo, em QC considerada boa nos relatos.
- **Fotos:** sala e jantar funcionais, cozinha planejada, três quartos claros e banheiros conservados. O quintal permanece majoritariamente aberto, com cobertura leve e uso social simples; é uma das soluções menos arriscadas para ventilação e manutenção.
- **Pendências:** confirmar proteção contra chuva, privacidade, área de serviço, taxas, orientação solar e regularização de qualquer cobertura.
- **Ação:** candidato forte; visitar.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-10-1346017
### 4. Anúncio 1397378 — QC 8
- **Anunciado:** R$ 600.000; 68 m²; condomínio R$ 316,14; IPTU R$ 1.119,67; 3 quartos, 1 suíte; 2 vagas.
- **Por que se destaca:** QC 8 tem acesso prático a comércio e serviços nos mapas fornecidos, e a casa combina preço intermediário com implantação próxima da planta original de três quartos.
- **Fotos:** sala e cozinha claras, marcenaria utilizável, três dormitórios aparentes e escada/andar superior compatíveis com a planta de 68 m². O fundo mantém trecho aberto, corredor lateral e cobertura parcial, sem transformar todo o quintal em segunda cozinha. Há sinais de acabamento simples e manutenção pontual, mas não de reforma integral imediata.
- **Pendências:** confirmar planta e documentos, origem das marcas/acabamentos, drenagem linear, impermeabilização da cobertura, orientação solar e itens de marcenaria que permanecem. Parte final da galeria está contaminada por fotos de outros imóveis e foi desconsiderada.
- **Ação:** candidato forte; visitar depois dos três primeiros.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-8-rua-b-1397378
### 5. Anúncio 1359960 — QC 7
- **Anunciado:** R$ 610.000; 68 m²; condomínio R$ 339; IPTU R$ 1.128,19; 3 quartos, 1 suíte; 2 vagas.
- **Por que entra:** QC considerada alternativa prática e conversão do quintal que cria uma sala social ampla, em vez de apenas instalar equipamentos com rótulo “gourmet”.
- **Fotos:** boa sala frontal, cozinha planejada, três quartos aparentes e grande espaço coberto nos fundos, hoje usado para descanso e convivência. Não foi identificada segunda cozinha completa; a ampliação parece flexível e mobiliável.
- **Pendências:** o fechamento é extenso; testar calor, ventilação cruzada, iluminação noturna, exaustão, acústica e impermeabilização antes de valorizá-lo.
- **Ação:** visitar com foco no conforto ambiental da ampliação.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-7-1359960
### 6. Anúncio 1319996 — QC 14
- **Anunciado:** R$ 649.000; 68 m²; condomínio e IPTU ausentes; 3 quartos, 1 suíte; 2 vagas.
- **Por que se destaca:** uma das reformas mais completas e coerentes, com placas solares e boa separação entre convivência e lavanderia.
- **Fotos:** acabamento uniforme, marcenaria abundante, três dormitórios aparentes, banheiros atualizados e área externa coberta com bancada, churrasqueira, abertura lateral e circulação suficiente para receber pessoas.
- **Pendências:** QC 14 não está entre as QCs priorizadas pelos relatos; confirmar taxas, homologação das placas, ventilação/exaustão, manutenção da cobertura e escopo técnico da reforma.
- **Ação:** visitar como referência de imóvel pronto.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-14-1319996
### 7. Anúncio 1324014 — QC 12
- **Anunciado:** R$ 649.000; 68 m²; condomínio e IPTU ausentes; 3 quartos, 1 suíte; vaga não extraída.
- **Por que entra:** acabamento interno e marcenaria fortes, com área social coberta bem executada e condição aparente de ocupação sem reforma relevante.
- **Fotos:** cozinha, sala e banheiros atualizados; a área gourmet é ampla e iluminada, mas concentra churrasqueira, bancada e apoio muito próximos da cozinha principal.
- **Pendências:** confirmar vaga, planta original, ventilação, exaustão, conforto térmico, drenagem, regularização da cobertura e se a duplicação de equipamentos melhora de fato a rotina.
- **Ação:** visitar depois dos candidatos mais baratos e das QCs prioritárias.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-12-rua-k-1324014
### 8. Anúncio 1333569 — QC 6
- **Anunciado:** R$ 650.000; 90 m²; condomínio e IPTU ausentes; 3 quartos, 1 suíte; 2 vagas.
- **Por que entra:** QC considerada alternativa prática e ampliação social já em uso, com abertura superior e lateral que pode mitigar o fechamento do quintal.
- **Fotos:** casa ocupada, bem conservada e funcional; a área externa reúne jantar, estar e churrasqueira, com cobertura aparentemente translúcida/retrátil. Os cômodos íntimos parecem utilizáveis, embora menos bem documentados que nos primeiros colocados.
- **Pendências:** confirmar se os 90 m² são comparáveis, funcionamento e manutenção da cobertura, exaustão, calor, iluminação, taxas e regularização.
- **Ação:** visitar depois dos oito primeiros.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-6-1333569
### 9. Anúncio 1359480 — QC 4
- **Anunciado:** R$ 535.000; 70 m²; condomínio R$ 330; IPTU ausente; 3 quartos, 1 suíte; 2 vagas.
- **Por que entra:** é a alternativa de menor preço entre os finalistas habitáveis e preserva margem financeira para custos de aquisição e adequações.
- **Fotos:** sala integrada utilizável, cozinha e banheiros funcionais e três dormitórios aparentes. O quintal/lavanderia têm acabamento mais simples que os líderes, mas não sugerem obra estrutural imediata nas imagens.
- **Pendências:** QC 4 precisa provar conveniência logística; confirmar planta original, área de 70 m², documentação, drenagem, impermeabilização e orçamento de acabamento externo.
- **Ação:** visitar como referência de valor e limite inferior de preço.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-4-1359480
### 10. Anúncio 1356197 — QC 4
- **Anunciado:** R$ 649.000; 68 m²; condomínio R$ 316; IPTU ausente; 3 quartos, 1 suíte; 2 vagas.
- **Por que entra:** casa vazia com acabamento interno forte e condição aparente de ocupação sem reforma relevante.
- **Fotos:** sala, cozinha, banheiros e marcenaria bem conservados; boa parte do espaço coberto parece funcionar como garagem, com menor evidência de quintal social útil.
- **Pendências:** confirmar planta original, uso e ventilação do espaço coberto, drenagem, impermeabilização, regularização e conveniência da QC 4.
- **Ação:** visitar depois dos nove primeiros.
- **URL:** https://www.dfimoveis.com.br/imovel/casa-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-4-1356197
## Quase entraram
- **[1368562](https://www.dfimoveis.com.br/imovel/casa-condominio-3-quartos-venda-jardins-mangueiral-brasilia-df-qc-5-1368562), QC 5, R$ 720.000:** ampliação de aproximadamente 110 m² bem executada e ventilada por cobertura translúcida, mas o prêmio de preço e a QC menos prioritária reduzem a urgência da visita.
## Quarentenas e exclusões relevantes
- **1351729:** a primeira foto declara “VENDIDO” e “negócio fechado”. Isso não comprova registro ou preço da transação, mas é evidência suficiente de indisponibilidade operacional; removido do ranking de visitas por correção do usuário em 17/07/2026.
- **1349519:** a primeira foto também declara “VENDIDO” e “negócio fechado”, com o código correspondente ao anúncio. Removido do ranking de visitas por correção do usuário em 17/07/2026.
- **1366801:** a primeira foto declara “VENDIDO” e “negócio fechado”, com o código 1468 correspondente ao anúncio. Removido do ranking de visitas por correção do usuário em 17/07/2026.
- **1346231:** a primeira foto declara “VENDIDO” e “negócio fechado”, com o código 1428 correspondente ao anúncio. Removido após auditoria sistemática das primeiras fotos em 17/07/2026.
- **1350829 e 1358463:** anunciados com quatro quartos; fora do filtro.
- **1374012:** a descrição declara que um dos três quartos é adaptado; não há prova suficiente de planta original de três quartos.
- **1351663:** preço estruturado de R$ 570 milhões, erro evidente de escala; excluído até correção.
- **1369473 e 1336887:** galerias dominadas por material genérico ou de outros anúncios, sem evidência suficiente do imóvel.
- **1301400 e 1201070:** fotos com marcação de vendido/permuta; tratadas como inconsistência, não prova de venda.
- **1041635 e 1383513:** mesmo imóvel provável pelas fotos; a repetição não foi contada como duas opções.
- **1239730, 1242337, 1294027, 1359192, 1359662 e 1367341:** grupo de republicações prováveis da mesma unidade; a oferta não foi inflada.
- **1354016 e 1377683:** duplicata provável após revisão visual; seria uma única opção, não duas.
- **1356153, 1358854 e 1358920:** grupo provável de uma mesma casa vazia.
- **1348449 e 1351461:** descrições e composição praticamente idênticas na QC 13; tratar como mesmo imóvel até confirmação.
## Qualidade e limitações
- O banco contém anúncios e snapshots de conteúdo, não imóveis físicos confirmados. Os dois pontos observados ainda são insuficientes para tendência ou tempo real de mercado.
- Entre os 84 anúncios ativos do Mangueiral, condomínio falta em 40 e IPTU em 74; não foi possível comparar custo mensal com consistência.
- Área existe em todos os registros, mas não é semanticamente comparável. Os próprios finalistas exibem divergências entre banco e descrição.
- A busca e as galerias contêm material contaminante do portal. Duplicidades foram classificadas manualmente e permanecem probabilísticas.
- Anúncio, foto e descrição não confirmam planta original, regularização da ampliação, documentação, estrutura ou sistemas.
- As preferências por QC vêm de relatos anedóticos e precisam ser testadas em deslocamentos e visitas reais.
- A referência financeira de R$ 650 mil a R$ 750 mil não foi assumida como atual. O ranking prioriza visitas, não define capacidade de compra ou preço máximo.
## Verificações obrigatórias nas visitas
1. solicitar planta original, matrícula, habite-se e documentos das ampliações;
2. medir os três quartos e conferir posição da escada, fachada e implantação;
3. testar calor, ventilação, luz e ruído com portas e janelas fechadas e abertas;
4. inspecionar telhado, rufos, calhas, impermeabilização, drenagem e sinais de infiltração;
5. testar quadro elétrico, aterramento, tomadas, hidráulica, pressão e escoamento;
6. verificar se a área gourmet duplica a cozinha ou realmente melhora a rotina;
7. confirmar condomínio, IPTU, vagas, taxas extras, regras de obra e atas;
8. percorrer a QC em horários relevantes para comércio, trânsito, segurança, ruído e estacionamento;
9. só depois estimar adequação e definir preço máximo de negociação.
## Reprodutibilidade
- `analysis.py`: extrai banco e HTMLs em leitura, produzindo TSV.
- `photo_similarity.py`: sinaliza pares por hash visual simples; exige revisão humana e sofre com imagens genéricas do portal.
- `query.sql`: consultas principais de universo e qualidade.
- `parameters.json`: parâmetros e fingerprint do snapshot.
- `results.csv`: ranking em formato tabular.

BIN
reports/mangueiral_top10/__pycache__/analysis.cpython-312.pyc

Binary file not shown.

BIN
reports/mangueiral_top10/__pycache__/analysis.cpython-314.pyc

Binary file not shown.

BIN
reports/mangueiral_top10/__pycache__/photo_similarity.cpython-312.pyc

Binary file not shown.

BIN
reports/mangueiral_top10/__pycache__/photo_similarity.cpython-314.pyc

Binary file not shown.

130
reports/mangueiral_top10/analysis.py

@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Read-only extraction helper for the Jardins Mangueiral ranking."""
from __future__ import annotations
import argparse
import gzip
import re
import sqlite3
import sys
from pathlib import Path
from bs4 import BeautifulSoup
ROOT = Path(__file__).resolve().parents[2]
DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3"
DATA_ROOT = DB_PATH.parent
SEARCH_FRAGMENT = "/jardins-mangueiral/"
def clean(value: str | None) -> str:
return re.sub(r"\s+", " ", value or "").strip()
def sanitize_description(value: str) -> str:
value = re.sub(r"\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b", "[email]", value)
value = re.sub(
r"(?:\(\d{2}\)|\b\d{2}\b)\s*(?:\d[\s.-]*){5,11}\d",
"[telefone]",
value,
)
value = re.sub(r"\bCRECI\b.{0,30}", "", value, flags=re.IGNORECASE)
return clean(value)
def parse_html(relative_path: str) -> tuple[str, str, str]:
with gzip.open(DATA_ROOT / relative_path, "rt", encoding="utf-8", errors="replace") as fh:
soup = BeautifulSoup(fh.read(), "lxml")
description_node = soup.select_one("div.assined-imv")
description = ""
if description_node:
for node in description_node.select("span, a, button"):
node.decompose()
description = sanitize_description(description_node.get_text(" ", strip=True))
details: list[str] = []
for item in soup.select("ul.details-text li"):
text = clean(item.get_text(" ", strip=True))
if text and text not in details:
details.append(text)
address_node = soup.select_one('[itemprop="address"]')
address = clean(address_node.get_text(" ", strip=True) if address_node else "")
return description, " | ".join(details), address
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--ids-only", action="store_true")
args = parser.parse_args()
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
connection.execute("PRAGMA query_only = ON")
connection.execute("BEGIN")
rows = connection.execute(
"""
SELECT
l.listing_id, l.url, l.price_brl, l.condominium_brl, l.iptu_brl,
l.area_m2, l.bedrooms, l.suites, l.parking_spaces,
l.raw_html_path, COUNT(p.source_url) AS photo_count
FROM listings AS l
JOIN listing_searches AS s USING (listing_id)
LEFT JOIN photos AS p USING (listing_id)
WHERE instr(s.search_url, ?) > 0
AND l.inactive_at IS NULL
GROUP BY l.listing_id
ORDER BY l.price_brl, l.listing_id
LIMIT 150
""",
(SEARCH_FRAGMENT,),
).fetchall()
connection.close()
if args.ids_only:
for row in rows:
print(row[0])
return 0
print(
"listing_id\tprice_brl\tcondominium_brl\tiptu_brl\tarea_m2\tbedrooms\t"
"suites\tparking_spaces\tphoto_count\taddress\tdetails\tdescription\turl"
)
for row in rows:
(
listing_id,
url,
price,
condominium,
iptu,
area,
bedrooms,
suites,
parking,
raw_path,
photo_count,
) = row
description, details, address = parse_html(raw_path)
values = (
listing_id,
price,
condominium,
iptu,
area,
bedrooms,
suites,
parking,
photo_count,
address,
details,
description,
url,
)
print("\t".join("" if value is None else clean(str(value)) for value in values))
return 0
if __name__ == "__main__":
sys.exit(main())

29
reports/mangueiral_top10/parameters.json

@ -0,0 +1,29 @@
{
"analysis_date": "2026-07-17",
"listing_data_reference_date": "2026-07-17",
"database_path": "dfimoveis_data/dfimoveis.sqlite3",
"database_main_file_sha256": "7a5102a62bc66a344bd9440fa36301d10a57711bcdd263b767b65e88f70957f4",
"logical_snapshot_sha256": "fe604aab8af7f67c833e6de2a0fc81c7dc2950ca440ad7b23ebd03d0c2546188",
"database_mode": "read_only_transaction_with_wal",
"search_url_fragment": "/jardins-mangueiral/",
"listing_limit": 150,
"observed_listing_count": 94,
"active_listing_count": 84,
"inactive_listing_count": 10,
"database_crawl_run_count": 16,
"new_listing_count_since_previous_report": 3,
"active_plausible_price_three_bedroom_count": 80,
"ranking_unit": "probable_physical_property",
"numeric_score_used": false,
"original_three_bedroom_requires_document_confirmation": true,
"photo_first_pass_limit_per_listing": 16,
"photo_first_pass_files": 1449,
"photo_finalist_count": 19,
"photo_finalist_files": 747,
"photo_files_reviewed_distinct_cumulative": 1907,
"new_finalist_photos_reviewed": 15,
"plans_reviewed": 2,
"map_captures_reviewed": 6,
"first_photo_sale_marker_audit_count": 10,
"visually_unavailable_listing_ids": ["1346231", "1349519", "1351729", "1366801"]
}

87
reports/mangueiral_top10/photo_similarity.py

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Suggest duplicate Mangueiral ads from visually similar local photos.
Uses a simple difference hash as a screening signal. Results are candidates for
manual review, never automatic proof that two ads describe the same property.
"""
from __future__ import annotations
import sqlite3
from collections import defaultdict
from pathlib import Path
from PIL import Image, UnidentifiedImageError
ROOT = Path(__file__).resolve().parents[2]
DB_PATH = ROOT / "dfimoveis_data" / "dfimoveis.sqlite3"
DATA_ROOT = DB_PATH.parent
def difference_hash(path: Path) -> int | None:
try:
with Image.open(path) as image:
pixels = list(image.convert("L").resize((9, 8)).getdata())
except (OSError, UnidentifiedImageError):
return None
value = 0
for row in range(8):
offset = row * 9
for column in range(8):
value = (value << 1) | (pixels[offset + column] > pixels[offset + column + 1])
return value
def main() -> None:
connection = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
connection.execute("PRAGMA query_only = ON")
connection.execute("BEGIN")
rows = connection.execute(
"""
WITH mangueiral AS (
SELECT s.listing_id
FROM listing_searches AS s
JOIN listings AS l USING (listing_id)
WHERE instr(search_url, '/jardins-mangueiral/') > 0
AND l.inactive_at IS NULL
), uncommon AS (
SELECT sha256
FROM photos
WHERE sha256 IS NOT NULL
GROUP BY sha256
HAVING COUNT(DISTINCT listing_id) <= 5
)
SELECT p.listing_id, p.local_path
FROM photos AS p
JOIN mangueiral AS m USING (listing_id)
JOIN uncommon AS u USING (sha256)
WHERE p.local_path IS NOT NULL
ORDER BY p.listing_id, p.ordinal
"""
).fetchall()
connection.close()
hashes: dict[str, list[int]] = defaultdict(list)
for listing_id, relative_path in rows:
value = difference_hash(DATA_ROOT / relative_path)
if value is not None:
hashes[listing_id].append(value)
ids = sorted(hashes)
print("listing_a\tlisting_b\tvisually_similar_photos")
for index, first_id in enumerate(ids):
for second_id in ids[index + 1 :]:
available = list(hashes[second_id])
matches = 0
for first_hash in hashes[first_id]:
distances = [(first_hash ^ second_hash).bit_count() for second_hash in available]
if distances and min(distances) <= 6:
available.pop(distances.index(min(distances)))
matches += 1
if matches >= 3:
print(f"{first_id}\t{second_id}\t{matches}")
if __name__ == "__main__":
main()

65
reports/mangueiral_top10/query.sql

@ -0,0 +1,65 @@
PRAGMA query_only = ON;
-- Universo capturado pela busca do Jardins Mangueiral.
SELECT
l.listing_id,
l.url,
l.price_brl,
l.condominium_brl,
l.iptu_brl,
l.area_m2,
l.bedrooms,
l.suites,
l.parking_spaces,
l.first_seen_at,
l.last_seen_at,
l.raw_html_path
FROM listings AS l
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/jardins-mangueiral/') > 0
AND l.inactive_at IS NULL
ORDER BY l.price_brl, l.listing_id
LIMIT 150;
-- Cardinalidade de fotos por anúncio. A contagem inclui material genérico do portal.
SELECT p.listing_id, COUNT(*) AS photo_count
FROM photos AS p
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/jardins-mangueiral/') > 0
AND EXISTS (
SELECT 1 FROM listings AS l
WHERE l.listing_id = p.listing_id AND l.inactive_at IS NULL
)
GROUP BY p.listing_id
ORDER BY p.listing_id
LIMIT 150;
-- Número de snapshots por anúncio e cobertura temporal.
SELECT
h.listing_id,
COUNT(*) AS snapshot_count,
MIN(h.captured_at) AS first_capture,
MAX(h.captured_at) AS last_capture
FROM listing_history AS h
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/jardins-mangueiral/') > 0
AND EXISTS (
SELECT 1 FROM listings AS l
WHERE l.listing_id = h.listing_id AND l.inactive_at IS NULL
)
GROUP BY h.listing_id
ORDER BY h.listing_id
LIMIT 150;
-- Campos ausentes no universo.
SELECT
COUNT(*) AS listing_count,
SUM(l.condominium_brl IS NULL) AS missing_condominium,
SUM(l.iptu_brl IS NULL) AS missing_iptu,
SUM(l.area_m2 IS NULL) AS missing_area,
SUM(l.parking_spaces IS NULL) AS missing_parking
FROM listings AS l
JOIN listing_searches AS s USING (listing_id)
WHERE instr(s.search_url, '/jardins-mangueiral/') > 0
AND l.inactive_at IS NULL
LIMIT 1;

11
reports/mangueiral_top10/results.csv

@ -0,0 +1,11 @@
rank,representative_listing_id,probable_duplicate_ids,qc,price_brl,area_m2,bedrooms,suites,parking_spaces,original_three_bedroom_status,decision
1,1279892,,10,595000,68,3,1,1,probable_pending_documents,strong_candidate
2,1350367,,6,569000,68,3,1,2,probable_pending_documents,strong_candidate
3,1346017,,10,577000,68,3,1,2,probable_pending_documents,strong_candidate
4,1397378,,8,600000,68,3,1,2,probable_pending_documents,strong_candidate
5,1359960,,7,610000,68,3,1,2,probable_pending_documents,visit_and_test_closed_yard
6,1319996,,14,649000,68,3,1,2,probable_pending_documents,visit
7,1324014,,12,649000,68,3,1,,probable_pending_documents,visit
8,1333569,,6,650000,90,3,1,2,probable_pending_documents,visit_after_top_eight
9,1359480,,4,535000,70,3,1,2,probable_pending_documents,visit_as_value_reference
10,1356197,,4,649000,68,3,1,2,probable_pending_documents,visit_after_top_nine

9
requirements-dfimoveis.txt

@ -0,0 +1,9 @@
httpx[http2]>=0.27,<1
beautifulsoup4>=4.12,<5
lxml>=5,<7
# Used by the report photo-similarity screening scripts.
Pillow>=10,<13
# Default browser automation (uses the locally installed Microsoft Edge):
playwright>=1.49,<2
cloudscraper>=1.2,<2
patchright>=1.0,<2

BIN
resources/maps/00aecaae-3e6b-4605-958f-70dbdb2b24b8.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

BIN
resources/maps/1d424088-057c-43e8-9459-306978d0a456.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
resources/maps/643d59ab-9deb-48b8-8b8f-f3d444aab4a2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

BIN
resources/maps/6f86274e-fb86-4d74-b526-fe8fc0f34e07.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

BIN
resources/maps/7c219f0a-933e-4643-9c50-c83081a75677.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
resources/maps/photo_2026-07-16_17-17-11.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

BIN
resources/maps/photo_2026-07-16_17-17-37.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
resources/maps/photo_2026-07-16_17-17-44.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
resources/maps/photo_2026-07-16_17-21-28.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
resources/maps/photo_2026-07-16_17-21-32.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
resources/maps/photo_2026-07-16_17-21-35.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
resources/plans/74920737-ED7F-4E08-A7C9-C8F3C92C598A.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
resources/plans/B65F8041-DA75-4982-A5B5-15BB2B8AB465.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

1
response.html

File diff suppressed because one or more lines are too long

3
search_list.txt

@ -0,0 +1,3 @@
# One complete DFImoveis search URL per line
https://www.dfimoveis.com.br/venda/df/brasilia/jardins-mangueiral/casa/3,4-quartos
https://www.dfimoveis.com.br/venda/df/cruzeiro/novo/apartamento/3-quartos

19
used_commands.ps1

@ -0,0 +1,19 @@
# change working directory
Set-Location E:\forge\python\house-quest
# install dependencies
.\.venv-scraper\Scripts\python.exe -m pip install -r requirements-dfimoveis.txt
# clean up browser profile
Remove-Item -Recurse -Force .\dfimoveis_data\browser-profile
# run Chrome with remote debugging enabled
& "C:\Program Files\Google\Chrome\Application\chrome.exe"`
--remote-debugging-port=9222 `
--user-data-dir="E:\forge\python\house-quest\dfimoveis_data\browser-profile"
# scraper execution command
.\.venv-scraper\Scripts\python.exe .\dfimoveis_scraper.py `
--search-file .\search_list.txt `
--output .\dfimoveis_data `
--attach-chrome
Loading…
Cancel
Save